服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
334 lines
11 KiB
Go
334 lines
11 KiB
Go
package framework
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Role 表示框架边界中的服务角色。
|
|
type Role string
|
|
|
|
const (
|
|
FrontendRole Role = "frontend"
|
|
BackendRole Role = "backend"
|
|
MeshRole Role = "mesh"
|
|
)
|
|
|
|
// Contract 是可由工具校验的实时服务边界规范。
|
|
type Contract struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
Frontend Boundary `json:"frontend"`
|
|
Backends []Boundary `json:"backends"`
|
|
RemoteServices []RemoteService `json:"remote_services"`
|
|
GroupServices []GroupService `json:"group_services"`
|
|
Session SessionContract `json:"session"`
|
|
Invariants []Invariant `json:"invariants"`
|
|
}
|
|
|
|
// Boundary 描述单个服务边界、职责,以及它提供或消费的规范。
|
|
type Boundary struct {
|
|
Service string `json:"service"`
|
|
Role Role `json:"role"`
|
|
Responsibilities []string `json:"responsibilities"`
|
|
Provides []string `json:"provides"`
|
|
Consumes []string `json:"consumes"`
|
|
}
|
|
|
|
// RemoteService 描述跨服务规范及其背后的传输语义。
|
|
type RemoteService struct {
|
|
Name string `json:"name"`
|
|
Transport string `json:"transport"`
|
|
Provider string `json:"provider"`
|
|
Consumers []string `json:"consumers"`
|
|
Guarantees []string `json:"guarantees"`
|
|
}
|
|
|
|
// GroupService 描述多个后端服务共同使用的运行时分组规范。
|
|
type GroupService struct {
|
|
Name string `json:"name"`
|
|
Runtime string `json:"runtime"`
|
|
Backends []string `json:"backends"`
|
|
Semantics []string `json:"semantics"`
|
|
}
|
|
|
|
// SessionContract 定义前端会话状态的归属、定位和重放规则。
|
|
type SessionContract struct {
|
|
Owner string `json:"owner"`
|
|
Binding string `json:"binding"`
|
|
Locator string `json:"locator"`
|
|
Events []string `json:"events"`
|
|
ClosePolicy string `json:"close_policy"`
|
|
ReplayPolicy string `json:"replay_policy"`
|
|
}
|
|
|
|
// Invariant 记录服务和运行时演进中必须保持成立的规则。
|
|
type Invariant struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// DefaultRealtimeContract 返回 gateway/logic/playerd/scene/social 的默认实时边界规范。
|
|
func DefaultRealtimeContract() Contract {
|
|
contract := Contract{
|
|
SchemaVersion: 1,
|
|
Frontend: Boundary{
|
|
Service: "gateway",
|
|
Role: FrontendRole,
|
|
Responsibilities: []string{
|
|
"transport_accept",
|
|
"login_handshake",
|
|
"session_binding",
|
|
"backend_route_forward",
|
|
"server_push",
|
|
},
|
|
Provides: []string{
|
|
"frontend_session",
|
|
"gateway_push",
|
|
"transport_snapshot",
|
|
"presence_publish",
|
|
},
|
|
Consumes: []string{
|
|
"logic_route_manifest",
|
|
"logic_player_response",
|
|
"registry_backend_nodes",
|
|
},
|
|
},
|
|
Backends: []Boundary{
|
|
{
|
|
Service: "logic",
|
|
Role: BackendRole,
|
|
Responsibilities: []string{
|
|
"stateful_player_handlers",
|
|
"idempotent_request_guard",
|
|
"profile_runtime",
|
|
"session_event_projection",
|
|
},
|
|
Provides: []string{
|
|
"route_manifest",
|
|
"logic_player_response",
|
|
"remote_rpc_handlers",
|
|
},
|
|
Consumes: []string{
|
|
"gateway_client_packet",
|
|
"session_connected",
|
|
"session_disconnected",
|
|
},
|
|
},
|
|
{
|
|
Service: "playerd",
|
|
Role: BackendRole,
|
|
Responsibilities: []string{
|
|
"profile_flush",
|
|
"player_loop_worker",
|
|
"storage_deadletter_requeue",
|
|
},
|
|
Provides: []string{"player_storage_admin"},
|
|
Consumes: []string{"profile_dirty_event"},
|
|
},
|
|
{
|
|
Service: "scene",
|
|
Role: MeshRole,
|
|
Responsibilities: []string{"room_allocation", "scene_event_projection"},
|
|
Provides: []string{"room_allocated"},
|
|
Consumes: []string{"room_allocate_request"},
|
|
},
|
|
{
|
|
Service: "social",
|
|
Role: MeshRole,
|
|
Responsibilities: []string{"chat_broadcast", "social_event_projection"},
|
|
Provides: []string{"chat_broadcast"},
|
|
Consumes: []string{"chat_send_request"},
|
|
},
|
|
},
|
|
RemoteServices: []RemoteService{
|
|
{
|
|
Name: "reliable_rpc",
|
|
Transport: "event_bus",
|
|
Provider: "internal/platform/rpc.Endpoint",
|
|
Consumers: []string{
|
|
"gateway",
|
|
"logic",
|
|
"scene",
|
|
"social",
|
|
"playerd",
|
|
},
|
|
Guarantees: []string{
|
|
"request_id_correlation",
|
|
"timeout_bounded_pending",
|
|
"discovery_resolved_target",
|
|
"trace_span_capture",
|
|
},
|
|
},
|
|
},
|
|
GroupServices: []GroupService{
|
|
{
|
|
Name: "runtime_group",
|
|
Runtime: "internal/platform/group.Runtime",
|
|
Backends: []string{"memory", "redis"},
|
|
Semantics: []string{
|
|
"create",
|
|
"ttl_renew",
|
|
"membership",
|
|
"broadcast",
|
|
"multicast",
|
|
},
|
|
},
|
|
},
|
|
Session: SessionContract{
|
|
Owner: "gateway",
|
|
Binding: "one_active_session_per_account",
|
|
Locator: "presence_tracker + registry node metadata",
|
|
Events: []string{"OnBind", "AfterBind", "OnClose", "OnKick", "OnReconnect"},
|
|
ClosePolicy: "new same-account bind closes stale frontend session",
|
|
ReplayPolicy: "idempotent backend response cache replays duplicate stateful packets",
|
|
},
|
|
Invariants: []Invariant{
|
|
{Name: "frontend owns connection state", Description: "Only gateway owns sockets and session connection objects."},
|
|
{Name: "backend owns gameplay state", Description: "Logic/playerd/scene/social own state mutations and never write client sockets directly."},
|
|
{Name: "push crosses explicit contract", Description: "Backend push must use GatewayPush or RPC/event bus contracts."},
|
|
{Name: "stateful routes are idempotent", Description: "Stateful authorized player routes must declare route policy and use idempotency guard."},
|
|
},
|
|
}
|
|
return Normalize(contract)
|
|
}
|
|
|
|
func Normalize(contract Contract) Contract {
|
|
normalizeBoundary(&contract.Frontend)
|
|
for i := range contract.Backends {
|
|
normalizeBoundary(&contract.Backends[i])
|
|
}
|
|
sort.Slice(contract.Backends, func(i, j int) bool {
|
|
if contract.Backends[i].Role == contract.Backends[j].Role {
|
|
return contract.Backends[i].Service < contract.Backends[j].Service
|
|
}
|
|
return contract.Backends[i].Role < contract.Backends[j].Role
|
|
})
|
|
for i := range contract.RemoteServices {
|
|
contract.RemoteServices[i].Name = strings.TrimSpace(contract.RemoteServices[i].Name)
|
|
contract.RemoteServices[i].Transport = strings.TrimSpace(contract.RemoteServices[i].Transport)
|
|
contract.RemoteServices[i].Provider = strings.TrimSpace(contract.RemoteServices[i].Provider)
|
|
contract.RemoteServices[i].Consumers = cleanSorted(contract.RemoteServices[i].Consumers)
|
|
contract.RemoteServices[i].Guarantees = cleanSorted(contract.RemoteServices[i].Guarantees)
|
|
}
|
|
sort.Slice(contract.RemoteServices, func(i, j int) bool {
|
|
return contract.RemoteServices[i].Name < contract.RemoteServices[j].Name
|
|
})
|
|
for i := range contract.GroupServices {
|
|
contract.GroupServices[i].Name = strings.TrimSpace(contract.GroupServices[i].Name)
|
|
contract.GroupServices[i].Runtime = strings.TrimSpace(contract.GroupServices[i].Runtime)
|
|
contract.GroupServices[i].Backends = cleanSorted(contract.GroupServices[i].Backends)
|
|
contract.GroupServices[i].Semantics = cleanSorted(contract.GroupServices[i].Semantics)
|
|
}
|
|
sort.Slice(contract.GroupServices, func(i, j int) bool {
|
|
return contract.GroupServices[i].Name < contract.GroupServices[j].Name
|
|
})
|
|
contract.Session.Events = cleanSorted(contract.Session.Events)
|
|
return contract
|
|
}
|
|
|
|
func Validate(contract Contract) error {
|
|
contract = Normalize(contract)
|
|
var missing []string
|
|
if contract.SchemaVersion <= 0 {
|
|
missing = append(missing, "schema_version")
|
|
}
|
|
if err := validateBoundary("frontend", contract.Frontend, FrontendRole); err != nil {
|
|
missing = append(missing, err.Error())
|
|
}
|
|
services := map[string]struct{}{}
|
|
if contract.Frontend.Service != "" {
|
|
services[contract.Frontend.Service] = struct{}{}
|
|
}
|
|
if len(contract.Backends) == 0 {
|
|
missing = append(missing, "backends")
|
|
}
|
|
for _, backend := range contract.Backends {
|
|
if _, ok := services[backend.Service]; ok {
|
|
missing = append(missing, fmt.Sprintf("duplicate service %s", backend.Service))
|
|
}
|
|
if backend.Service != "" {
|
|
services[backend.Service] = struct{}{}
|
|
}
|
|
if backend.Role != BackendRole && backend.Role != MeshRole {
|
|
missing = append(missing, fmt.Sprintf("backend %s role must be backend or mesh", backend.Service))
|
|
}
|
|
if err := validateBoundary("backend", backend, ""); err != nil {
|
|
missing = append(missing, err.Error())
|
|
}
|
|
}
|
|
if len(contract.RemoteServices) == 0 {
|
|
missing = append(missing, "remote_services")
|
|
}
|
|
for _, remote := range contract.RemoteServices {
|
|
if remote.Name == "" || remote.Transport == "" || remote.Provider == "" {
|
|
missing = append(missing, "remote_service name/transport/provider")
|
|
}
|
|
for _, consumer := range remote.Consumers {
|
|
if _, ok := services[consumer]; !ok {
|
|
missing = append(missing, fmt.Sprintf("remote_service %s unknown consumer %s", remote.Name, consumer))
|
|
}
|
|
}
|
|
}
|
|
if len(contract.GroupServices) == 0 {
|
|
missing = append(missing, "group_services")
|
|
}
|
|
for _, group := range contract.GroupServices {
|
|
if group.Name == "" || group.Runtime == "" || len(group.Backends) == 0 {
|
|
missing = append(missing, "group_service name/runtime/backends")
|
|
}
|
|
}
|
|
if contract.Session.Owner == "" || contract.Session.Binding == "" || contract.Session.Locator == "" {
|
|
missing = append(missing, "session owner/binding/locator")
|
|
}
|
|
if len(missing) > 0 {
|
|
return fmt.Errorf("framework contract incomplete: %s", strings.Join(missing, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBoundary(label string, boundary Boundary, role Role) error {
|
|
if boundary.Service == "" {
|
|
return fmt.Errorf("%s service", label)
|
|
}
|
|
if role != "" && boundary.Role != role {
|
|
return fmt.Errorf("%s %s role must be %s", label, boundary.Service, role)
|
|
}
|
|
if boundary.Role == "" {
|
|
return fmt.Errorf("%s %s role", label, boundary.Service)
|
|
}
|
|
if len(boundary.Responsibilities) == 0 {
|
|
return fmt.Errorf("%s %s responsibilities", label, boundary.Service)
|
|
}
|
|
if len(boundary.Provides) == 0 {
|
|
return fmt.Errorf("%s %s provides", label, boundary.Service)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeBoundary(boundary *Boundary) {
|
|
boundary.Service = strings.TrimSpace(boundary.Service)
|
|
boundary.Role = Role(strings.TrimSpace(string(boundary.Role)))
|
|
boundary.Responsibilities = cleanSorted(boundary.Responsibilities)
|
|
boundary.Provides = cleanSorted(boundary.Provides)
|
|
boundary.Consumes = cleanSorted(boundary.Consumes)
|
|
}
|
|
|
|
func cleanSorted(values []string) []string {
|
|
out := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|