feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics

Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

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

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

Improve automatic login and recovery diagnostics; document configuration and release flows.
This commit is contained in:
2026-10-05 11:50:31 +08:00
parent eb445f88f4
commit d8771caef2
222 changed files with 19000 additions and 4284 deletions
+4 -3
View File
@@ -30,6 +30,7 @@ type Options struct {
Versions clientconfig.ReleaseVersions
LocalIdentityPlugin string
LoginUIPlugin string
CashShopPlugin string
}
type Request struct {
@@ -266,7 +267,7 @@ func (s *Studio) Patch(input Request) (Response, error) {
func (s *Studio) Install(input Request) (Response, error) {
return s.perform("install plugins", func() (Response, error) {
result, err := clientsetup.InstallPlugins(input.GameDirectory, input.settings(), s.options.Versions, s.options.LocalIdentityPlugin, s.options.LoginUIPlugin)
result, err := clientsetup.InstallPlugins(input.GameDirectory, input.settings(), s.options.Versions, s.options.LocalIdentityPlugin, s.options.LoginUIPlugin, s.options.CashShopPlugin)
if err != nil {
return Response{}, err
}
@@ -274,7 +275,7 @@ func (s *Studio) Install(input Request) (Response, error) {
return Response{}, err
}
message := "BD2 client plugins installed or updated"
if !result.LocalIdentity.Changed && !result.LoginUI.Changed {
if !result.LocalIdentity.Changed && !result.LoginUI.Changed && !result.CashShop.Changed {
message = "BD2 client plugins are already up to date; no DLL was rewritten"
}
return success(message, result), nil
@@ -312,7 +313,7 @@ func (s *Studio) Launch(input Request) (Response, error) {
if !installation.SupportedOnHost() {
return Response{}, fmt.Errorf("cannot launch a %s game client from this operating system", installation.Kind)
}
for _, name := range []string{"BD2LocalIdentity.dll", "BD2LoginUI.dll"} {
for _, name := range []string{"BD2LocalIdentity.dll", "BD2LoginUI.dll", "BD2CashShop.dll"} {
info, err := os.Stat(filepath.Join(installation.Plugins, name))
if err != nil || !info.Mode().IsRegular() {
return Response{}, fmt.Errorf("install or update the client plugins before launching; %s is missing", name)
+1
View File
@@ -33,6 +33,7 @@ type ReleaseVersions struct {
LocalIdentity string `json:"local_identity"`
CaptureEnvironment string `json:"capture_environment"`
LoginUI string `json:"login_ui"`
CashShop string `json:"cash_shop"`
} `json:"plugins"`
}
+2 -1
View File
@@ -23,6 +23,7 @@ type Spec struct {
var (
LocalIdentity = Spec{fileName: "BD2LocalIdentity.dll"}
LoginUI = Spec{fileName: "BD2LoginUI.dll"}
CashShop = Spec{fileName: "BD2CashShop.dll"}
GameNames = Spec{fileName: "BD2.GameNames.dll"}
)
@@ -84,7 +85,7 @@ func Install(spec Spec, gameDir, source string) (Result, error) {
// Read both artifacts before touching the installation. The runtime table
// library is shipped beside every plugin, including explicit development paths.
var runtimeData []byte
if spec == LocalIdentity || spec == LoginUI {
if spec == LocalIdentity || spec == LoginUI || spec == CashShop {
runtimeSource := filepath.Join(filepath.Dir(source), GameNames.fileName)
runtimeData, err = os.ReadFile(runtimeSource)
if err != nil {
+4 -4
View File
@@ -8,7 +8,7 @@ import (
)
func TestInstallRequiresBepInExWithoutCopyingPlugin(t *testing.T) {
for _, spec := range []Spec{LocalIdentity, LoginUI} {
for _, spec := range []Spec{LocalIdentity, LoginUI, CashShop} {
t.Run(spec.FileName(), func(t *testing.T) {
gameDir := t.TempDir()
for path, data := range map[string][]byte{
@@ -41,7 +41,7 @@ func TestInstallRequiresBepInExWithoutCopyingPlugin(t *testing.T) {
}
func TestInstallCopiesUpdatesAndSkipsIdenticalPlugin(t *testing.T) {
for _, spec := range []Spec{LocalIdentity, LoginUI} {
for _, spec := range []Spec{LocalIdentity, LoginUI, CashShop} {
t.Run(spec.FileName(), func(t *testing.T) {
gameDir := t.TempDir()
for path, data := range map[string][]byte{
@@ -140,7 +140,7 @@ func TestInstallKeepsPluginsSeparate(t *testing.T) {
t.Fatal(err)
}
}
for _, spec := range []Spec{LocalIdentity, LoginUI} {
for _, spec := range []Spec{LocalIdentity, LoginUI, CashShop} {
source := filepath.Join(t.TempDir(), spec.FileName())
if err := os.WriteFile(filepath.Join(filepath.Dir(source), GameNames.FileName()), []byte("names-v1"), 0o600); err != nil {
t.Fatal(err)
@@ -152,7 +152,7 @@ func TestInstallKeepsPluginsSeparate(t *testing.T) {
t.Fatal(err)
}
}
for _, spec := range []Spec{LocalIdentity, LoginUI} {
for _, spec := range []Spec{LocalIdentity, LoginUI, CashShop} {
path := filepath.Join(gameDir, "BepInEx", "plugins", spec.FileName())
data, err := os.ReadFile(path)
if err != nil || string(data) != spec.FileName() {
+11 -1
View File
@@ -50,6 +50,7 @@ type ResourcePolicy struct {
type InstallResult struct {
LocalIdentity plugin.Result `json:"local_identity"`
LoginUI plugin.Result `json:"login_ui"`
CashShop plugin.Result `json:"cash_shop"`
}
func Inspect(gameDir string, versions clientconfig.ReleaseVersions) (GameStatus, error) {
@@ -117,6 +118,7 @@ func InstallPlugins(
versions clientconfig.ReleaseVersions,
localIdentitySource string,
loginUISource string,
cashShopSource string,
) (InstallResult, error) {
status, err := Inspect(gameDir, versions)
if err != nil {
@@ -136,6 +138,10 @@ func InstallPlugins(
if err != nil {
return InstallResult{}, err
}
cashSource, err := plugin.ResolvePackaged(plugin.CashShop, cashShopSource)
if err != nil {
return InstallResult{}, err
}
local, err := plugin.Install(plugin.LocalIdentity, gameDir, localSource)
if err != nil {
return InstallResult{}, err
@@ -144,7 +150,11 @@ func InstallPlugins(
if err != nil {
return InstallResult{}, err
}
return InstallResult{LocalIdentity: local, LoginUI: login}, nil
cash, err := plugin.Install(plugin.CashShop, gameDir, cashSource)
if err != nil {
return InstallResult{}, err
}
return InstallResult{LocalIdentity: local, LoginUI: login, CashShop: cash}, nil
}
func FetchResourcePolicy(ctx context.Context, client *http.Client, settings clientconfig.Settings, versions clientconfig.ReleaseVersions) (ResourcePolicy, error) {