chore(all): resolve lint findings, update example plugin, refine docs and add LGPL licenses
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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/`:本地服务端客户端专用插件。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user