合并本地领先源码:旧UI、GM疲劳燃烧、白金徽章孔、宠物修复、支援兵普攻开关
This commit is contained in:
1 parent
f2debaf9bf
commit
70b3c5a6dc
57 files changed
+4256
-30
No files matched your search
@@ -0,0 +1,430 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
"longheng.io/server/internal/modules/dnf/party"
|
||||
dnffriend "longheng.io/server/internal/modules/dnf/unitedfriend"
|
||||
)
|
||||
|
||||
// Volunteers are persistent social characters: the native friend list and the
|
||||
// native party frame both resolve them through a synthetic presence session.
|
||||
// The session has no socket; it exists only while the server process is up so
|
||||
// the operator's friend window keeps seeing the volunteers as online, including
|
||||
// after a dungeon ends.
|
||||
const (
|
||||
currentCompanionNativePartyEnv = "DNF_COMPANION_NATIVE_PARTY"
|
||||
// Presence of this file disables the native op9/op153 frame without a
|
||||
// restart. The friend presence stays online; only the party window is
|
||||
// suppressed, which is the fast rollback if a client build rejects the
|
||||
// synthetic roster.
|
||||
currentCompanionNativePartyDisableFile = "companion_native_party.disabled"
|
||||
currentCompanionVirtualPrefix = "companion-presence-"
|
||||
)
|
||||
|
||||
func currentCompanionNativePartyEnabled() bool {
|
||||
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentCompanionNativePartyEnv))) {
|
||||
case "0", "off", "false", "no":
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(currentCompanionNativePartyDisableFile); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) companionPresenceSession(characterID uint16) (*gameSession, bool) {
|
||||
if s == nil || characterID == 0 {
|
||||
return nil, false
|
||||
}
|
||||
// A real client always wins over a synthetic presence row.
|
||||
if real, online := s.onlineGameSession(characterID); online && real != nil {
|
||||
return real, false
|
||||
}
|
||||
s.companionPresenceMu.Lock()
|
||||
defer s.companionPresenceMu.Unlock()
|
||||
session := s.companionSessions[characterID]
|
||||
return session, session != nil
|
||||
}
|
||||
|
||||
// companionPresenceFriendChannel returns the friend-row channel for a synthetic
|
||||
// volunteer. It copies the field under companionPresenceMu because ensure may
|
||||
// update the channel when the owner moves.
|
||||
func (s *Service) companionPresenceFriendChannel(characterID uint16) (uint16, bool) {
|
||||
if s == nil || characterID == 0 {
|
||||
return 0, false
|
||||
}
|
||||
if real, online := s.onlineGameSession(characterID); online && real != nil {
|
||||
return 0, false
|
||||
}
|
||||
s.companionPresenceMu.Lock()
|
||||
defer s.companionPresenceMu.Unlock()
|
||||
session := s.companionSessions[characterID]
|
||||
if session == nil {
|
||||
return 0, false
|
||||
}
|
||||
return uint16(session.channel.ID), true
|
||||
}
|
||||
|
||||
func (s *Service) companionSessionSnapshot(characterID uint16) (boundGameSessionCharacter, bool) {
|
||||
session, ok := s.companionPresenceSession(characterID)
|
||||
if !ok || session == nil || session.companionVirtual == false {
|
||||
return boundGameSessionCharacter{}, false
|
||||
}
|
||||
return boundGameSessionCharacter{
|
||||
session: session,
|
||||
character: characterID,
|
||||
generation: session.characterGeneration,
|
||||
virtual: true,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *Service) companionSessionCurrent(identity boundGameSessionCharacter) bool {
|
||||
if s == nil || identity.session == nil || identity.character == 0 {
|
||||
return false
|
||||
}
|
||||
s.companionPresenceMu.Lock()
|
||||
defer s.companionPresenceMu.Unlock()
|
||||
return s.companionSessions[identity.character] == identity.session &&
|
||||
identity.session.selectedCharacterID == identity.character &&
|
||||
identity.session.characterGeneration == identity.generation
|
||||
}
|
||||
|
||||
func (s *Service) nextCompanionGeneration() uint64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.nextGameSessionGeneration++
|
||||
if s.nextGameSessionGeneration == 0 {
|
||||
s.nextGameSessionGeneration++
|
||||
}
|
||||
return s.nextGameSessionGeneration
|
||||
}
|
||||
|
||||
// ensureCompanionPresenceForSession makes the currently configured volunteers
|
||||
// visible in the native friend window for this owner. It is called at the login
|
||||
// scene tail (before op273) and again at dungeon entry.
|
||||
func (s *Service) ensureCompanionPresenceForSession(ctx context.Context, session *gameSession) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 ||
|
||||
!currentDungeonCompanionTeamModeEnabled() {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil || repos.Settings == nil {
|
||||
return
|
||||
}
|
||||
ownerID := session.selectedCharacterID
|
||||
ownerKey := strconv.FormatUint(uint64(ownerID), 10)
|
||||
cfg, _, _, err := s.loadCurrentDungeonCompanionConfig(
|
||||
ctx, ownerKey, strings.TrimSpace(s.accountIDForSession(session)), repos)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-companion-presence-config-failed",
|
||||
"owner_character_id", ownerID, "error", err)
|
||||
return
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
s.dropCompanionPresenceForOwner(ownerID)
|
||||
return
|
||||
}
|
||||
sources := cfg.Sources()
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
if accountID == "" {
|
||||
owner, found, loadErr := repos.Character.Load(ctx, ownerKey)
|
||||
if loadErr != nil || !found {
|
||||
return
|
||||
}
|
||||
accountID = strings.TrimSpace(owner.AccountID)
|
||||
}
|
||||
matched, _, applied := s.autoMatchCurrentDungeonCompanionSources(
|
||||
ctx, repos, accountID, ownerID, sources)
|
||||
if applied {
|
||||
sources = matched
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
ownerName := ""
|
||||
if _, name, _, found := s.selectedCharacterForEnter(ctx, session); found {
|
||||
ownerName = name
|
||||
}
|
||||
s.ensureCompanionFriendRelations(ctx, session, ownerName, sources)
|
||||
if currentCompanionHeadlessEnabled() {
|
||||
ids := s.ensureHeadlessCompanionSessions(ctx, session, sources, accountID)
|
||||
s.ensureHeadlessCompanionParty(session, ids)
|
||||
} else {
|
||||
s.dropHeadlessCompanionSessionsForOwner(ownerID)
|
||||
s.ensureCompanionVirtualSessions(ctx, session, sources, accountID)
|
||||
}
|
||||
s.logGameEvent(session, "game-companion-presence-online",
|
||||
"owner_character_id", ownerID,
|
||||
"source_character_ids", strings.Join(sources, ","),
|
||||
"count", len(sources))
|
||||
}
|
||||
|
||||
func (s *Service) ensureCompanionVirtualSessions(
|
||||
ctx context.Context,
|
||||
ownerSession *gameSession,
|
||||
sources []string,
|
||||
accountID string,
|
||||
) {
|
||||
if s == nil || ownerSession == nil || len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.Character == nil {
|
||||
return
|
||||
}
|
||||
for _, raw := range sources {
|
||||
id, ok := currentDungeonCompanionProjectionSourceIDValue(raw)
|
||||
if !ok || id == ownerSession.selectedCharacterID {
|
||||
continue
|
||||
}
|
||||
if _, realOnline := s.onlineGameSession(id); realOnline {
|
||||
continue
|
||||
}
|
||||
character, found, err := repos.Character.Load(ctx, strconv.FormatUint(uint64(id), 10))
|
||||
if err != nil || !found || character.Stats["delete_flag"] != 0 ||
|
||||
strings.TrimSpace(character.Name) == "" {
|
||||
continue
|
||||
}
|
||||
// Allocate the generation before taking companionPresenceMu: the
|
||||
// generation owner lives under Service.mu, and bindGameSessionCharacter
|
||||
// holds Service.mu while dropping a virtual session.
|
||||
generation := s.nextCompanionGeneration()
|
||||
s.companionPresenceMu.Lock()
|
||||
if s.companionSessions == nil {
|
||||
s.companionSessions = make(map[uint16]*gameSession)
|
||||
}
|
||||
session := s.companionSessions[id]
|
||||
if session == nil {
|
||||
session = &gameSession{
|
||||
connID: currentCompanionVirtualPrefix + strconv.FormatUint(uint64(id), 10),
|
||||
accountID: accountID,
|
||||
selectedCharacterID: id,
|
||||
characterGeneration: generation,
|
||||
channel: ownerSession.channel,
|
||||
companionVirtual: true,
|
||||
companionOwnerID: ownerSession.selectedCharacterID,
|
||||
}
|
||||
s.companionSessions[id] = session
|
||||
} else {
|
||||
session.channel = ownerSession.channel
|
||||
session.companionOwnerID = ownerSession.selectedCharacterID
|
||||
session.accountID = accountID
|
||||
}
|
||||
s.companionPresenceMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ensureCompanionFriendRelations(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
ownerName string,
|
||||
sources []string,
|
||||
) {
|
||||
if s == nil || session == nil || strings.TrimSpace(ownerName) == "" {
|
||||
return
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.UnitedFriend == nil || repos.Character == nil {
|
||||
return
|
||||
}
|
||||
owner, err := dnffriend.NewOwner(repos, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ownerID := session.selectedCharacterID
|
||||
s.friendMu.Lock()
|
||||
defer s.friendMu.Unlock()
|
||||
for _, raw := range sources {
|
||||
id, ok := currentDungeonCompanionProjectionSourceIDValue(raw)
|
||||
if !ok || id == ownerID {
|
||||
continue
|
||||
}
|
||||
character, found, loadErr := repos.Character.Load(ctx, strconv.FormatUint(uint64(id), 10))
|
||||
if loadErr != nil || !found || strings.TrimSpace(character.Name) == "" {
|
||||
continue
|
||||
}
|
||||
if _, addErr := owner.Add(ctx, ownerName, ownerID, character.Name, id); addErr != nil {
|
||||
if addErr != dnffriend.ErrFull {
|
||||
s.logGameEvent(session, "game-companion-friend-add-failed",
|
||||
"owner_character_id", ownerID,
|
||||
"source_character_id", id,
|
||||
"error", addErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.logGameEvent(session, "game-companion-friend-ensured",
|
||||
"owner_character_id", ownerID,
|
||||
"source_character_id", id,
|
||||
"name", character.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) dropCompanionPresenceCharacter(characterID uint16) {
|
||||
if s == nil || characterID == 0 {
|
||||
return
|
||||
}
|
||||
s.companionPresenceMu.Lock()
|
||||
defer s.companionPresenceMu.Unlock()
|
||||
delete(s.companionSessions, characterID)
|
||||
}
|
||||
|
||||
func (s *Service) dropCompanionPresenceForOwner(ownerID uint16) {
|
||||
if s == nil || ownerID == 0 {
|
||||
return
|
||||
}
|
||||
s.companionPresenceMu.Lock()
|
||||
defer s.companionPresenceMu.Unlock()
|
||||
for id, session := range s.companionSessions {
|
||||
if session != nil && session.companionOwnerID == ownerID {
|
||||
delete(s.companionSessions, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func companionPartyID(ownerID uint16) uint16 {
|
||||
id := uint16(0xE000) | (ownerID & 0x0FFF)
|
||||
if id == 0 || id == 0xFFFF {
|
||||
id = 0xE001
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *Service) buildCompanionPartyState(session *gameSession, run *currentDungeonCompanionRun) alignedcmd.PartyState {
|
||||
if s == nil || session == nil || run == nil {
|
||||
return alignedcmd.PartyState{}
|
||||
}
|
||||
state := alignedcmd.PartyState{
|
||||
PartyID: int(companionPartyID(session.selectedCharacterID)),
|
||||
IsLeader: true,
|
||||
UserID: session.selectedCharacterID,
|
||||
UserState: 100,
|
||||
MaxMembers: 4,
|
||||
}
|
||||
state.Members = append(state.Members, alignedcmd.PartyMemberState{
|
||||
UserID: session.selectedCharacterID, Slot: 0, UserState: 100, HPPercent: 100, MPPercent: 100,
|
||||
})
|
||||
slot := byte(1)
|
||||
for _, entry := range run.Sources {
|
||||
if !entry.Ready || entry.SourceCharacterID == 0 {
|
||||
continue
|
||||
}
|
||||
if slot >= state.MaxMembers {
|
||||
break
|
||||
}
|
||||
state.Members = append(state.Members, alignedcmd.PartyMemberState{
|
||||
UserID: entry.SourceCharacterID, Slot: slot, UserState: 100, HPPercent: 100, MPPercent: 100,
|
||||
})
|
||||
slot++
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// publishCurrentDungeonCompanionNativeParty builds the native op9 party frame
|
||||
// (kind-0 complete roster) followed by op153 HP/MP rows. It intentionally does
|
||||
// not send op11: the volunteers have no P2P socket, and a made-up endpoint
|
||||
// would leave the client stuck on "connecting". The scene actors themselves
|
||||
// still come from the support-soldier projection.
|
||||
func (s *Service) publishCurrentDungeonCompanionNativeParty(session *gameSession, runtime *runtimeDungeonState) {
|
||||
if s == nil || session == nil || runtime == nil || runtime.Companion == nil ||
|
||||
currentCompanionHeadlessEnabled() ||
|
||||
!currentCompanionNativePartyEnabled() {
|
||||
return
|
||||
}
|
||||
run := runtime.Companion
|
||||
if !run.Ready || len(run.Sources) == 0 {
|
||||
return
|
||||
}
|
||||
currentParty := runtimePartyStateSnapshot(session)
|
||||
if currentParty.PartyID > 0 && len(runtimePartyMembers(currentParty)) > 1 {
|
||||
s.logGameEvent(session, "game-companion-native-party-skipped",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"reason", "owner_already_in_native_party")
|
||||
return
|
||||
}
|
||||
if runtime.partyDungeonOwner != nil || isCurrentImplementedRaidChannel(session) {
|
||||
s.logGameEvent(session, "game-companion-native-party-skipped",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"reason", "party_or_raid_owner_has_priority")
|
||||
return
|
||||
}
|
||||
sources := make([]string, 0, len(run.Sources))
|
||||
for _, entry := range run.Sources {
|
||||
if entry.Ready && entry.SourceCharacterID != 0 {
|
||||
sources = append(sources, strconv.FormatUint(uint64(entry.SourceCharacterID), 10))
|
||||
}
|
||||
}
|
||||
s.ensureCompanionVirtualSessions(
|
||||
context.Background(), session, sources, strings.TrimSpace(s.accountIDForSession(session)))
|
||||
state := s.buildCompanionPartyState(session, run)
|
||||
if len(state.Members) <= 1 {
|
||||
return
|
||||
}
|
||||
sent, err := s.sendCurrentPartyActorFrameProjection(
|
||||
session, session, state, "companion_native_party")
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-companion-native-party-failed",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", state.PartyID,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
if !sent {
|
||||
s.logGameEvent(session, "game-companion-native-party-deferred",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", state.PartyID,
|
||||
"reason", "op9_projection_not_delivered")
|
||||
return
|
||||
}
|
||||
if err := s.sendRuntimePartyRealtimeInfoLocal(session, state); err != nil {
|
||||
s.logGameEvent(session, "game-companion-native-party-rows-failed",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", state.PartyID,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
run.NativePartyID = uint16(state.PartyID)
|
||||
run.NativePartySent = true
|
||||
s.logGameEvent(session, "game-companion-native-party-sent",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", state.PartyID,
|
||||
"member_count", len(state.Members),
|
||||
"source_character_ids", uint16ListLog(currentDungeonCompanionTeamSourceIDs(run)))
|
||||
}
|
||||
|
||||
// clearCurrentDungeonCompanionNativeParty empties only the party window; the
|
||||
// virtual sessions and friend rows stay online, which is the requested
|
||||
// post-dungeon state.
|
||||
func (s *Service) clearCurrentDungeonCompanionNativeParty(session *gameSession, run *currentDungeonCompanionRun) {
|
||||
if s == nil || session == nil || run == nil || !run.NativePartySent || run.NativePartyID == 0 {
|
||||
return
|
||||
}
|
||||
body := buildCurrentSceneOp9ActorRemovalBodyInContext(
|
||||
run.NativePartyID, currentPartySceneOwnerContext(session))
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketRecoverStamina), body, 0); err != nil {
|
||||
s.logGameEvent(session, "game-companion-native-party-clear-failed",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", run.NativePartyID,
|
||||
"error", err)
|
||||
}
|
||||
if err := s.sendGameUpperRawClassCodec(
|
||||
session, 0x0099, party.BuildEmptyRealtimeInfo(), 0, true); err != nil {
|
||||
s.logGameEvent(session, "game-companion-native-party-clear-rows-failed",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", run.NativePartyID,
|
||||
"error", err)
|
||||
}
|
||||
run.NativePartySent = false
|
||||
s.logGameEvent(session, "game-companion-native-party-cleared",
|
||||
"owner_character_id", session.selectedCharacterID,
|
||||
"party_id", run.NativePartyID,
|
||||
"friends_online", true)
|
||||
}
|
||||
Reference in new issue
Block a user