服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
588 lines
20 KiB
Go
588 lines
20 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const clientAccountRegistrationTTL = 24 * time.Hour
|
|
|
|
const clientAccountRegistrationGrace = time.Second
|
|
|
|
type clientAccountRegistration struct {
|
|
accountID string
|
|
expiresAt time.Time
|
|
remoteClientPID uint32
|
|
spendTimeEventInfoAuthorized bool
|
|
spendTimeEventInfoSendState uint8
|
|
selectedChannelKnown bool
|
|
selectedChannelID int
|
|
selectedChannelType uint8
|
|
// selectedCharacterID is the character this client most recently selected.
|
|
// A reconnect transport that reports a different one is a fresh login.
|
|
selectedCharacterID uint16
|
|
// launcherIdentityAttested records that this registration was accompanied
|
|
// by a valid launcher identity proof. It is the only flag the enforcing
|
|
// game-connection gate accepts, so an operator (or a player) who can reach
|
|
// the loopback admin token but does not hold the launcher's private key
|
|
// still cannot seat a session.
|
|
launcherIdentityAttested bool
|
|
}
|
|
|
|
const (
|
|
spendTimeEventInfoUnsent uint8 = iota
|
|
spendTimeEventInfoSending
|
|
spendTimeEventInfoSent
|
|
)
|
|
|
|
type clientAccountRegistry struct {
|
|
mu sync.Mutex
|
|
byPID map[uint32]clientAccountRegistration
|
|
byRemoteIP map[netip.Addr]clientAccountRegistration
|
|
}
|
|
|
|
func newClientAccountRegistry() clientAccountRegistry {
|
|
return clientAccountRegistry{
|
|
byPID: make(map[uint32]clientAccountRegistration),
|
|
byRemoteIP: make(map[netip.Addr]clientAccountRegistration),
|
|
}
|
|
}
|
|
|
|
func normalizeRemoteClientAccountIP(value string) (netip.Addr, error) {
|
|
address, err := netip.ParseAddr(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return netip.Addr{}, errors.New("remote client IP is invalid")
|
|
}
|
|
address = address.Unmap()
|
|
if !address.Is4() || !address.IsPrivate() || address.IsLoopback() {
|
|
return netip.Addr{}, errors.New("remote client IP must be a private non-loopback IPv4 address")
|
|
}
|
|
return address, nil
|
|
}
|
|
|
|
// RegisterRemoteClientAccount binds one SSH-authenticated Windows client to
|
|
// the private WireGuard source address observed by the Linux game listeners.
|
|
// The remote PID is retained only as audit metadata: it is not a process
|
|
// identity on the Linux host and may collide with another player's PID.
|
|
func (s *Service) RegisterRemoteClientAccount(pid uint32, clientIP, accountID string) error {
|
|
if s == nil {
|
|
return errors.New("dnf bridge is unavailable")
|
|
}
|
|
address, err := normalizeRemoteClientAccountIP(clientIP)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountID = strings.TrimSpace(accountID)
|
|
if accountID == "" {
|
|
return errors.New("client account ID is required")
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
if s.clientAccounts.byRemoteIP == nil {
|
|
s.clientAccounts.byRemoteIP = make(map[netip.Addr]clientAccountRegistration)
|
|
}
|
|
for registeredIP, entry := range s.clientAccounts.byRemoteIP {
|
|
if !entry.expiresAt.After(now) {
|
|
delete(s.clientAccounts.byRemoteIP, registeredIP)
|
|
}
|
|
}
|
|
entry, found := s.clientAccounts.byRemoteIP[address]
|
|
if found && entry.expiresAt.After(now) && strings.TrimSpace(entry.accountID) == accountID &&
|
|
entry.remoteClientPID == pid && entry.spendTimeEventInfoAuthorized &&
|
|
entry.spendTimeEventInfoSendState == spendTimeEventInfoUnsent {
|
|
entry.expiresAt = now.Add(clientAccountRegistrationTTL)
|
|
s.clientAccounts.byRemoteIP[address] = entry
|
|
return nil
|
|
}
|
|
s.clientAccounts.byRemoteIP[address] = clientAccountRegistration{
|
|
accountID: accountID,
|
|
expiresAt: now.Add(clientAccountRegistrationTTL),
|
|
remoteClientPID: pid,
|
|
spendTimeEventInfoAuthorized: true,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) registeredRemoteClientAccount(address netip.Addr) (string, uint32, bool) {
|
|
if s == nil {
|
|
return "", 0, false
|
|
}
|
|
address = address.Unmap()
|
|
if !address.Is4() || !address.IsPrivate() || address.IsLoopback() {
|
|
return "", 0, false
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
entry, found := s.clientAccounts.byRemoteIP[address]
|
|
if !found {
|
|
return "", 0, false
|
|
}
|
|
if !entry.expiresAt.After(now) {
|
|
delete(s.clientAccounts.byRemoteIP, address)
|
|
return "", 0, false
|
|
}
|
|
accountID := strings.TrimSpace(entry.accountID)
|
|
return accountID, entry.remoteClientPID, accountID != ""
|
|
}
|
|
|
|
func (s *Service) waitRegisteredRemoteClientAccount(
|
|
ctx context.Context,
|
|
address netip.Addr,
|
|
wait time.Duration,
|
|
) (string, uint32, bool) {
|
|
if accountID, pid, found := s.registeredRemoteClientAccount(address); found || wait <= 0 {
|
|
return accountID, pid, found
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", 0, false
|
|
case <-timer.C:
|
|
return s.registeredRemoteClientAccount(address)
|
|
case <-ticker.C:
|
|
if accountID, pid, found := s.registeredRemoteClientAccount(address); found {
|
|
return accountID, pid, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) sessionClientAccountRegistrationLocked(session *gameSession) (clientAccountRegistration, bool) {
|
|
if session == nil {
|
|
return clientAccountRegistration{}, false
|
|
}
|
|
if address := session.accountSourceIP.Unmap(); address.IsValid() {
|
|
entry, found := s.clientAccounts.byRemoteIP[address]
|
|
return entry, found
|
|
}
|
|
entry, found := s.clientAccounts.byPID[session.clientPID]
|
|
return entry, found
|
|
}
|
|
|
|
func (s *Service) storeSessionClientAccountRegistrationLocked(session *gameSession, entry clientAccountRegistration) {
|
|
if address := session.accountSourceIP.Unmap(); address.IsValid() {
|
|
if s.clientAccounts.byRemoteIP == nil {
|
|
s.clientAccounts.byRemoteIP = make(map[netip.Addr]clientAccountRegistration)
|
|
}
|
|
s.clientAccounts.byRemoteIP[address] = entry
|
|
return
|
|
}
|
|
if s.clientAccounts.byPID == nil {
|
|
s.clientAccounts.byPID = make(map[uint32]clientAccountRegistration)
|
|
}
|
|
s.clientAccounts.byPID[session.clientPID] = entry
|
|
}
|
|
|
|
func (s *Service) deleteSessionClientAccountRegistrationLocked(session *gameSession) {
|
|
if address := session.accountSourceIP.Unmap(); address.IsValid() {
|
|
delete(s.clientAccounts.byRemoteIP, address)
|
|
return
|
|
}
|
|
delete(s.clientAccounts.byPID, session.clientPID)
|
|
}
|
|
|
|
// RegisterClientAccount binds one launcher-authenticated local DNF process to
|
|
// its repository account. The loopback authenticated admin endpoint is the
|
|
// only production caller; no password or session secret enters the bridge.
|
|
func (s *Service) RegisterClientAccount(pid uint32, accountID string) error {
|
|
return s.RegisterClientAccountAttested(pid, accountID, false)
|
|
}
|
|
|
|
// RegisterClientAccountAttested is RegisterClientAccount plus the launcher
|
|
// identity result. attested=true is set only by the admin endpoint after it has
|
|
// verified the deployment's public key over (server_id, pid, account, expiry).
|
|
// No other writer may set it, because it is what the enforcing game gate trusts.
|
|
func (s *Service) RegisterClientAccountAttested(
|
|
pid uint32,
|
|
accountID string,
|
|
attested bool,
|
|
) error {
|
|
if s == nil {
|
|
return errors.New("dnf bridge is unavailable")
|
|
}
|
|
accountID = strings.TrimSpace(accountID)
|
|
if pid == 0 {
|
|
return errors.New("client PID must be positive")
|
|
}
|
|
if accountID == "" {
|
|
return errors.New("client account ID is required")
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
if s.clientAccounts.byPID == nil {
|
|
s.clientAccounts.byPID = make(map[uint32]clientAccountRegistration)
|
|
}
|
|
for registeredPID, entry := range s.clientAccounts.byPID {
|
|
if !entry.expiresAt.After(now) {
|
|
delete(s.clientAccounts.byPID, registeredPID)
|
|
}
|
|
}
|
|
entry, found := s.clientAccounts.byPID[pid]
|
|
if found && entry.expiresAt.After(now) && strings.TrimSpace(entry.accountID) == accountID &&
|
|
entry.spendTimeEventInfoAuthorized &&
|
|
entry.spendTimeEventInfoSendState == spendTimeEventInfoUnsent {
|
|
// A transport retry before the new process reaches op108 is idempotent.
|
|
// Once sending has begun, a later launcher registration is a new process
|
|
// lifecycle (including OS PID reuse) and must receive a fresh gate.
|
|
entry.expiresAt = now.Add(clientAccountRegistrationTTL)
|
|
// An attested registration must never be downgraded by a later
|
|
// unattested retry of the same process; that would let a second local
|
|
// caller strip the proof off a live session.
|
|
entry.launcherIdentityAttested = entry.launcherIdentityAttested || attested
|
|
s.clientAccounts.byPID[pid] = entry
|
|
return nil
|
|
}
|
|
s.clientAccounts.byPID[pid] = clientAccountRegistration{
|
|
accountID: accountID,
|
|
expiresAt: now.Add(clientAccountRegistrationTTL),
|
|
spendTimeEventInfoAuthorized: true,
|
|
launcherIdentityAttested: attested,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) registeredClientAccount(pid uint32) (string, bool) {
|
|
entry, found := s.registeredClientAccountEntry(pid)
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return strings.TrimSpace(entry.accountID), true
|
|
}
|
|
|
|
// registeredClientAccountEntry returns the live registration for one PID,
|
|
// including the launcher identity flag. Expired entries are dropped on read.
|
|
func (s *Service) registeredClientAccountEntry(pid uint32) (clientAccountRegistration, bool) {
|
|
if s == nil || pid == 0 {
|
|
return clientAccountRegistration{}, false
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
entry, found := s.clientAccounts.byPID[pid]
|
|
if !found {
|
|
return clientAccountRegistration{}, false
|
|
}
|
|
if !entry.expiresAt.After(now) {
|
|
delete(s.clientAccounts.byPID, pid)
|
|
return clientAccountRegistration{}, false
|
|
}
|
|
if strings.TrimSpace(entry.accountID) == "" {
|
|
return clientAccountRegistration{}, false
|
|
}
|
|
return entry, true
|
|
}
|
|
|
|
// registeredClientAccountForPID resolves the TCP-owning game process back to
|
|
// the process that the launcher registered. The current client can move its
|
|
// game sockets into a short-lived child process, so requiring an exact PID
|
|
// match silently falls back to the instance account even though the launcher
|
|
// authenticated a different local account.
|
|
func (s *Service) registeredClientAccountForPID(pid uint32) (accountID string, found bool, direct bool) {
|
|
return s.registeredClientAccountForCandidates(clientAccountProcessLineage(pid))
|
|
}
|
|
|
|
func (s *Service) registeredClientAccountForCandidates(candidates []uint32) (accountID string, found bool, direct bool) {
|
|
for index, candidate := range candidates {
|
|
if accountID, found := s.registeredClientAccount(candidate); found {
|
|
return accountID, true, index == 0
|
|
}
|
|
}
|
|
return "", false, false
|
|
}
|
|
|
|
// attestedClientAccountForCandidates is the launcher-identity-aware form of
|
|
// registeredClientAccountForCandidates. It reports whether the matching
|
|
// registration carried a launcher identity proof, which is the only question the
|
|
// enforcing game gate asks.
|
|
func (s *Service) attestedClientAccountForCandidates(
|
|
candidates []uint32,
|
|
) (accountID string, found bool, direct bool, attested bool) {
|
|
for index, candidate := range candidates {
|
|
entry, ok := s.registeredClientAccountEntry(candidate)
|
|
if !ok {
|
|
continue
|
|
}
|
|
return strings.TrimSpace(entry.accountID), true, index == 0, entry.launcherIdentityAttested
|
|
}
|
|
return "", false, false, false
|
|
}
|
|
|
|
// waitAttestedClientAccountForPID mirrors waitRegisteredClientAccountForPID but
|
|
// also reports the launcher identity result, so the game gate can wait out the
|
|
// same process-start race without giving up the binding requirement.
|
|
func (s *Service) waitAttestedClientAccountForPID(
|
|
ctx context.Context,
|
|
pid uint32,
|
|
wait time.Duration,
|
|
) (accountID string, found bool, direct bool, attested bool) {
|
|
lookup := func() (string, bool, bool, bool) {
|
|
return s.attestedClientAccountForCandidates(clientAccountProcessLineage(pid))
|
|
}
|
|
if accountID, found, direct, attested = lookup(); found || pid == 0 || wait <= 0 {
|
|
return accountID, found, direct, attested
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", false, false, false
|
|
case <-timer.C:
|
|
return lookup()
|
|
case <-ticker.C:
|
|
if accountID, found, direct, attested = lookup(); found {
|
|
return accountID, true, direct, attested
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// observeCurrentClientSelectedChannel records the last character-bearing game
|
|
// channel for one launcher-authenticated client process. Channel switches may
|
|
// close and unbind the old socket before the replacement selects its character,
|
|
// so the online-player/session indexes are not authoritative for detecting a
|
|
// team-channel return. The authenticated process registration survives that
|
|
// short reconnect gap and is reset naturally for a new client process.
|
|
func (s *Service) observeCurrentClientSelectedChannel(
|
|
session *gameSession,
|
|
characterID uint16,
|
|
) (previousID int, previousType uint8, found bool) {
|
|
if s == nil || session == nil || (session.clientPID == 0 && !session.accountSourceIP.IsValid()) {
|
|
return 0, 0, false
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
entry, registered := s.sessionClientAccountRegistrationLocked(session)
|
|
if !registered || !entry.expiresAt.After(now) || strings.TrimSpace(entry.accountID) == "" {
|
|
if registered && !entry.expiresAt.After(now) {
|
|
s.deleteSessionClientAccountRegistrationLocked(session)
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
previousID = entry.selectedChannelID
|
|
previousType = entry.selectedChannelType
|
|
found = entry.selectedChannelKnown
|
|
// A new character over the reconnect transport still owns a fresh login.
|
|
if session.channelReconnect && entry.selectedCharacterID != 0 && entry.selectedCharacterID != characterID {
|
|
session.channelReconnectNewCharacter = true
|
|
}
|
|
entry.selectedCharacterID = characterID
|
|
entry.selectedChannelKnown = true
|
|
entry.selectedChannelID = session.channel.ID
|
|
entry.selectedChannelType = session.channel.Type
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
return previousID, previousType, found
|
|
}
|
|
|
|
// waitRegisteredClientAccount closes the small process-start race between the
|
|
// launcher obtaining DNF.exe's PID and its authenticated loopback admin POST.
|
|
// Without this grace, a fast client could reach a game listener first and be
|
|
// permanently classified as an unbound all-channel discovery probe.
|
|
func (s *Service) waitRegisteredClientAccount(
|
|
ctx context.Context,
|
|
pid uint32,
|
|
wait time.Duration,
|
|
) (string, bool) {
|
|
if accountID, found := s.registeredClientAccount(pid); found || pid == 0 || wait <= 0 {
|
|
return accountID, found
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", false
|
|
case <-timer.C:
|
|
return s.registeredClientAccount(pid)
|
|
case <-ticker.C:
|
|
if accountID, found := s.registeredClientAccount(pid); found {
|
|
return accountID, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) waitRegisteredClientAccountForPID(
|
|
ctx context.Context,
|
|
pid uint32,
|
|
wait time.Duration,
|
|
) (accountID string, found bool, direct bool) {
|
|
if accountID, found, direct = s.registeredClientAccountForPID(pid); found || pid == 0 || wait <= 0 {
|
|
return accountID, found, direct
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", false, false
|
|
case <-timer.C:
|
|
return s.registeredClientAccountForPID(pid)
|
|
case <-ticker.C:
|
|
if accountID, found, direct = s.registeredClientAccountForPID(pid); found {
|
|
return accountID, true, direct
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// uniqueRegisteredClientAccount returns the account only when every active
|
|
// launcher registration names the same account. Wine does not consistently
|
|
// expose owning PIDs through GetExtendedTcpTable; this bounded single-account
|
|
// projection preserves authenticated login without guessing between accounts.
|
|
func (s *Service) uniqueRegisteredClientAccount() (string, bool) {
|
|
accountID, _, found := s.uniqueRegisteredClientAccountFlag()
|
|
return accountID, found
|
|
}
|
|
|
|
// uniqueRegisteredClientAccountFlag also reports whether every agreeing
|
|
// registration carried a launcher identity proof. A mixed set (one attested,
|
|
// one not) counts as unattested: the enforcing gate must not inherit trust from
|
|
// a proof that only covered some of the live processes.
|
|
func (s *Service) uniqueRegisteredClientAccountFlag() (string, bool, bool) {
|
|
if s == nil {
|
|
return "", false, false
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
accountID := ""
|
|
attested := false
|
|
for pid, entry := range s.clientAccounts.byPID {
|
|
if !entry.expiresAt.After(now) {
|
|
delete(s.clientAccounts.byPID, pid)
|
|
continue
|
|
}
|
|
candidate := strings.TrimSpace(entry.accountID)
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
if accountID == "" {
|
|
accountID = candidate
|
|
attested = entry.launcherIdentityAttested
|
|
continue
|
|
}
|
|
if accountID != candidate {
|
|
return "", false, false
|
|
}
|
|
attested = attested && entry.launcherIdentityAttested
|
|
}
|
|
return accountID, attested, accountID != ""
|
|
}
|
|
|
|
func (s *Service) waitUniqueRegisteredClientAccount(
|
|
ctx context.Context,
|
|
wait time.Duration,
|
|
) (string, bool) {
|
|
if accountID, found := s.uniqueRegisteredClientAccount(); found || wait <= 0 {
|
|
return accountID, found
|
|
}
|
|
timer := time.NewTimer(wait)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", false
|
|
case <-timer.C:
|
|
return s.uniqueRegisteredClientAccount()
|
|
case <-ticker.C:
|
|
if accountID, found := s.uniqueRegisteredClientAccount(); found {
|
|
return accountID, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// beginCurrentSpendTimeEventInfo reserves the single process-lifetime op108
|
|
// descriptor. A second game session for the same local client PID must not
|
|
// emit the first-process base-catalog grammar again. The in-flight state also keeps
|
|
// concurrent reconnect sessions from sending progress ahead of that first
|
|
// descriptor. PID=0 is not authoritative and therefore remains progress-only.
|
|
func (s *Service) beginCurrentSpendTimeEventInfo(session *gameSession) (send bool, ready bool) {
|
|
if s == nil || session == nil {
|
|
return false, false
|
|
}
|
|
if session.clientPID == 0 && !session.accountSourceIP.IsValid() {
|
|
session.spendTime.eventInfoSent = true
|
|
return false, true
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
entry, found := s.sessionClientAccountRegistrationLocked(session)
|
|
if !found || !entry.expiresAt.After(now) {
|
|
entry = clientAccountRegistration{expiresAt: now.Add(clientAccountRegistrationTTL)}
|
|
}
|
|
if !entry.spendTimeEventInfoAuthorized {
|
|
// An unknown PID can be an already-running client reconnecting after a
|
|
// server restart. Its singleton may already have consumed op108, so only
|
|
// the launcher-authenticated new-process registration may authorize the
|
|
// first-process grammar. Progress-only is the safe recovery behavior.
|
|
session.spendTime.eventInfoSent = true
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
return false, true
|
|
}
|
|
switch entry.spendTimeEventInfoSendState {
|
|
case spendTimeEventInfoSent:
|
|
session.spendTime.eventInfoSent = true
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
return false, true
|
|
case spendTimeEventInfoSending:
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
return false, false
|
|
default:
|
|
entry.spendTimeEventInfoSendState = spendTimeEventInfoSending
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
return true, true
|
|
}
|
|
}
|
|
|
|
// finishCurrentSpendTimeEventInfo commits or releases a reservation. sent=true
|
|
// must be used as soon as the op108 socket write succeeds, even if the
|
|
// following op1206 write fails; retrying the first-process grammar would then
|
|
// be invalid for that client process.
|
|
func (s *Service) finishCurrentSpendTimeEventInfo(session *gameSession, sent bool) {
|
|
if s == nil || session == nil {
|
|
return
|
|
}
|
|
if sent {
|
|
session.spendTime.eventInfoSent = true
|
|
}
|
|
if session.clientPID == 0 && !session.accountSourceIP.IsValid() {
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
s.clientAccounts.mu.Lock()
|
|
defer s.clientAccounts.mu.Unlock()
|
|
entry, found := s.sessionClientAccountRegistrationLocked(session)
|
|
if !found || !entry.expiresAt.After(now) {
|
|
entry = clientAccountRegistration{expiresAt: now.Add(clientAccountRegistrationTTL)}
|
|
}
|
|
if sent {
|
|
entry.spendTimeEventInfoSendState = spendTimeEventInfoSent
|
|
} else if entry.spendTimeEventInfoSendState == spendTimeEventInfoSending {
|
|
entry.spendTimeEventInfoSendState = spendTimeEventInfoUnsent
|
|
}
|
|
s.storeSessionClientAccountRegistrationLocked(session, entry)
|
|
}
|