DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,225 @@
|
||||
// Package dnf90 contains the single composition root for the DNF90 server.
|
||||
package dnf90
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
appkit "longheng.io/server/internal/platform/app"
|
||||
"longheng.io/server/internal/services/dnfbridge"
|
||||
"longheng.io/server/internal/services/dnfrobot"
|
||||
logicdnf "longheng.io/server/internal/services/logic/dnf"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceName = "dnfbridge"
|
||||
DefaultConfigPath = "configs/dnfbridge.toml"
|
||||
DefaultLogicPath = "configs/dnf/logic.toml"
|
||||
)
|
||||
|
||||
// Components wires storage before the protocol service.
|
||||
func Components(env *appkit.Env) ([]appkit.Component, error) {
|
||||
return components(env, nil)
|
||||
}
|
||||
|
||||
// ComponentsWithShutdown adds a loopback admin endpoint that cancels the
|
||||
// application context, allowing the Windows launcher to stop cleanly.
|
||||
func ComponentsWithShutdown(cancel context.CancelFunc) appkit.Builder {
|
||||
return func(env *appkit.Env) ([]appkit.Component, error) {
|
||||
return components(env, cancel)
|
||||
}
|
||||
}
|
||||
|
||||
func components(env *appkit.Env, cancel context.CancelFunc) ([]appkit.Component, error) {
|
||||
if cancel != nil && env != nil && env.AdminMux != nil {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
_, _ = w.Write([]byte("shutdown requested\n"))
|
||||
go cancel()
|
||||
})
|
||||
if env.AdminAuth != nil {
|
||||
handler = env.AdminAuth("platform", handler)
|
||||
}
|
||||
env.AdminMux.HandleFunc("/local/shutdown", handler)
|
||||
}
|
||||
|
||||
configPath := strings.TrimSpace(os.Getenv(logicdnf.EnvConfigPath))
|
||||
if configPath == "" {
|
||||
configPath = DefaultLogicPath
|
||||
}
|
||||
cfg, err := logicdnf.LoadConfigForEnv(configPath, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime, err := logicdnf.NewRuntime(env, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
robotsEnabled := dnfRobotsEnabled()
|
||||
robotSessionSecret := ""
|
||||
if robotsEnabled {
|
||||
robotSessionSecret = env.Core().Config.Admin.Token
|
||||
}
|
||||
bridge := dnfbridge.NewWithOptions(env, dnfbridge.ServiceOptions{
|
||||
RepositoryProvider: runtime.Group,
|
||||
LocalDebugMode: localDNFDebugModeEnabled(env),
|
||||
LocalDebugStrikeOnSkillUse: localDNFDebugStrikeOnSkillUseEnabled(env),
|
||||
RobotSessionSecret: robotSessionSecret,
|
||||
})
|
||||
if env != nil && env.AdminMux != nil {
|
||||
adminListen := strings.TrimSpace(env.Core().Config.Admin.Listen)
|
||||
if adminListen != "" {
|
||||
if err := bridge.ConfigurePaymentWeb("http://" + adminListen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env.AdminMux.Handle(dnfbridge.PaymentWebOrdinaryPath, bridge.PaymentWebHandler())
|
||||
}
|
||||
}
|
||||
registerDNFDebugRoutes(env, bridge)
|
||||
registerDNFGMRoutes(env, bridge)
|
||||
// 30CN GM 工具(GmTool90CN)需要的 /local 桥接能力:
|
||||
// /local/quest-resync、/local/dungeon-control、/local/character-clone。
|
||||
registerDNFGMToolCompatRoutes(env, bridge)
|
||||
// 支援兵(companion):/local/companion 读状态 + 写配置。
|
||||
// 配置按账号落库,运行态按副本冻结;真正的召唤走私有 opcode 0x7F41 投影。
|
||||
registerCompanionAdmin(env, bridge)
|
||||
registerGMAdminRoutes(env, bridge)
|
||||
var robotManager *dnfrobot.Manager
|
||||
if robotsEnabled {
|
||||
robotManager = dnfrobot.NewManager(env.Logger, runtime.Group, bridge, env.Core().Config.Admin.Token)
|
||||
registerDNFRobotGMRoutes(env, bridge, robotManager)
|
||||
}
|
||||
if env != nil && env.AdminMux != nil {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
PID uint32 `json:"pid"`
|
||||
AccountID string `json:"account_id"`
|
||||
ClientIP string `json:"client_ip,omitempty"`
|
||||
// LidLease is the launcher's signed statement over
|
||||
// (server_id, pid, account_id, expiry). It is optional so an
|
||||
// older launcher keeps working; when the server enforces the
|
||||
// launcher identity it is the difference between a seat and a
|
||||
// refusal.
|
||||
LidLease string `json:"lid_lease,omitempty"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
identity := bridge.LauncherIdentityInfo()
|
||||
lease := strings.TrimSpace(request.LidLease)
|
||||
var err error
|
||||
switch {
|
||||
case lease != "":
|
||||
// A lease names the launcher, so it takes precedence over the
|
||||
// legacy shapes: the client_ip form is the SSH-attested remote
|
||||
// path, and combining the two would need a proof over both
|
||||
// dimensions.
|
||||
attested, lidErr := bridge.VerifyLauncherIdentityLease(
|
||||
request.PID,
|
||||
request.AccountID,
|
||||
lease,
|
||||
)
|
||||
if lidErr != nil {
|
||||
if identity.Mode == dnfbridge.LauncherIdentityModeEnforce {
|
||||
// 401, never 400. A 400 is exactly what an older server
|
||||
// returns for an unknown JSON field, and the launcher
|
||||
// answers a 400 by retrying without the proof — which
|
||||
// would silently register an unattested PID that the game
|
||||
// gate then refuses, so the operator sees an unexplained
|
||||
// connect failure instead of "this launcher is not
|
||||
// authorized".
|
||||
http.Error(w, "launcher identity lease rejected: "+lidErr.Error(),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// Audit: record why, then keep the pre-feature behaviour.
|
||||
bridge.LogLauncherIdentityVerdict(request.PID, request.AccountID, lidErr)
|
||||
}
|
||||
err = bridge.RegisterClientAccountAttested(request.PID, request.AccountID, attested)
|
||||
case identity.Mode == dnfbridge.LauncherIdentityModeEnforce:
|
||||
// Enforce mode with no proof at all. Registering the account here
|
||||
// would only move the refusal to the game socket, where the
|
||||
// symptom is an unexplained connect failure.
|
||||
lidErr := bridge.RejectBareLauncherIdentityBind(request.PID, request.AccountID)
|
||||
http.Error(w, "launcher identity lease required: "+lidErr.Error(),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
case strings.TrimSpace(request.ClientIP) != "":
|
||||
err = bridge.RegisterRemoteClientAccount(request.PID, request.ClientIP, request.AccountID)
|
||||
default:
|
||||
err = bridge.RegisterClientAccount(request.PID, request.AccountID)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
if env.AdminAuth != nil {
|
||||
handler = env.AdminAuth("platform", handler)
|
||||
}
|
||||
env.AdminMux.HandleFunc("/local/client-account", handler)
|
||||
|
||||
// The launcher reads this before it signs anything: the server instance
|
||||
// ID is what stops a proof from one server run being replayed against
|
||||
// the next, and the mode tells the launcher whether a proof is required
|
||||
// or merely observed. It is admin-authenticated like every other
|
||||
// /local/* route, so only a caller that already holds the admin token
|
||||
// can read it.
|
||||
infoHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(bridge.LauncherIdentityInfo())
|
||||
})
|
||||
if env.AdminAuth != nil {
|
||||
infoHandler = env.AdminAuth("platform", infoHandler)
|
||||
}
|
||||
env.AdminMux.HandleFunc("/local/lid-info", infoHandler)
|
||||
}
|
||||
components := make([]appkit.Component, 0, 3)
|
||||
if runtime.Repository != nil {
|
||||
components = append(components, runtime.Repository)
|
||||
}
|
||||
components = append(components, bridge)
|
||||
if robotManager != nil {
|
||||
components = append(components, robotManager)
|
||||
}
|
||||
return components, nil
|
||||
}
|
||||
|
||||
func dnfRobotsEnabled() bool {
|
||||
value := strings.TrimSpace(os.Getenv("DNFBRIDGE_ROBOT_ENABLED"))
|
||||
return strings.EqualFold(value, "true")
|
||||
}
|
||||
|
||||
func localDNFDebugModeEnabled(env *appkit.Env) bool {
|
||||
if env == nil || !strings.EqualFold(env.Core().Config.Service.Environment, "local") {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(os.Getenv("DNFBRIDGE_DEBUG_MODE")), "true")
|
||||
}
|
||||
|
||||
func localDNFDebugStrikeOnSkillUseEnabled(env *appkit.Env) bool {
|
||||
return localDNFDebugModeEnabled(env) &&
|
||||
strings.EqualFold(strings.TrimSpace(os.Getenv("DNFBRIDGE_DEBUG_STRIKE_ON_SKILL_USE")), "true")
|
||||
}
|
||||
Reference in new issue
Block a user