193 lines
6.2 KiB
Go
193 lines
6.2 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
)
|
|
|
|
// Real-actor PoC for the volunteer squad.
|
|
//
|
|
// This path is deliberately switch-gated and OFF by default. It publishes each
|
|
// volunteer as a real character actor (mode0 + mode1 + mode3, objectKey =
|
|
// character id, non-zero party owner context) instead of the support-soldier
|
|
// APC projection. It is the first milestone of the "real character enters the
|
|
// dungeon" route; attaching the client AIC to these actors is the next client
|
|
// patch step, so this path currently ships appearance/roster only.
|
|
const (
|
|
currentCompanionRealActorsEnv = "DNF_COMPANION_REAL_ACTORS"
|
|
currentCompanionRealActorsFlagFile = "companion_real_actors.enabled"
|
|
currentCompanionRealActorsMode0Kind = "real_party_actor"
|
|
)
|
|
|
|
func currentCompanionRealActorsEnabled() bool {
|
|
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentCompanionRealActorsEnv))) {
|
|
case "0", "off", "false", "no":
|
|
return false
|
|
}
|
|
if _, err := os.Stat(currentCompanionRealActorsFlagFile); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// publishCurrentDungeonCompanionRealActors returns true only when every ready
|
|
// volunteer was published as a real actor. A partial failure returns false so
|
|
// the caller can keep the proven projection path instead of showing a broken
|
|
// half-team.
|
|
func (s *Service) publishCurrentDungeonCompanionRealActors(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
) bool {
|
|
if s == nil || session == nil || runtime == nil || runtime.Companion == nil ||
|
|
!currentCompanionRealActorsEnabled() {
|
|
return false
|
|
}
|
|
run := runtime.Companion
|
|
if !run.Ready || len(run.Sources) == 0 {
|
|
return false
|
|
}
|
|
ownerContext := currentPartySceneOwnerContext(session)
|
|
if ownerContext == 0 && session.channel.ID > 0 && session.channel.ID <= 0xff {
|
|
ownerContext = byte(session.channel.ID)
|
|
}
|
|
if ownerContext == 0 {
|
|
s.logGameEvent(session, "game-companion-real-actors-skipped",
|
|
"owner_character_id", session.selectedCharacterID,
|
|
"reason", "party_owner_context_not_committed")
|
|
return false
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
|
defer cancel()
|
|
type realRow struct {
|
|
id uint16
|
|
name string
|
|
mode0 []byte
|
|
mode1 []byte
|
|
}
|
|
rows := make([]realRow, 0, len(run.Sources))
|
|
for _, entry := range run.Sources {
|
|
if !entry.Ready || entry.SourceCharacterID == 0 {
|
|
continue
|
|
}
|
|
state := entry.ProgressionSnapshot
|
|
if state.Source.CharacterID == "" {
|
|
return false
|
|
}
|
|
name := strings.TrimSpace(state.Source.Name)
|
|
if name == "" {
|
|
name = entry.SourceName
|
|
}
|
|
mode0, err := s.buildCurrentSceneObjectListBodyForSessionInContextStrict(
|
|
ctx, session, entry.SourceCharacterID, name, state.Source, true, ownerContext)
|
|
if err != nil || len(mode0) < 3 {
|
|
s.logGameEvent(session, "game-companion-real-actor-failed",
|
|
"source_character_id", entry.SourceCharacterID,
|
|
"stage", "mode0", "error", err)
|
|
return false
|
|
}
|
|
reader := csharpLegacyUserInfoReader{
|
|
ctx: ctx,
|
|
characterID: state.Source.CharacterID,
|
|
service: s,
|
|
session: session,
|
|
suppressActorSkillResources: true,
|
|
equipmentSnapshot: &state.Equipment,
|
|
}
|
|
mode1 := reader.buildCurrentUserInfoMode1WithEquipmentForOwner(
|
|
state.Source, true, entry.SourceCharacterID, true, 0,
|
|
currentSceneObjectRoute, ownerContext)
|
|
if len(mode1) < 3 {
|
|
s.logGameEvent(session, "game-companion-real-actor-failed",
|
|
"source_character_id", entry.SourceCharacterID,
|
|
"stage", "mode1")
|
|
return false
|
|
}
|
|
rows = append(rows, realRow{id: entry.SourceCharacterID, name: name, mode0: mode0, mode1: mode1})
|
|
}
|
|
if len(rows) == 0 {
|
|
return false
|
|
}
|
|
// The client's native multi-row reader creates scene actors for a party
|
|
// prelude. Sending one row at a time produced global visuals that a dungeon
|
|
// room never rendered; the roster must be one counted mode0/mode1 body.
|
|
var multiMode0, multiMode1 packetWriter
|
|
multiMode0.writeByte(0)
|
|
multiMode0.writeUint16(uint16(len(rows)))
|
|
multiMode1.writeByte(1)
|
|
multiMode1.writeUint16(uint16(len(rows)))
|
|
for _, row := range rows {
|
|
multiMode0.writeBytes(row.mode0[3:])
|
|
multiMode1.writeBytes(row.mode1[3:])
|
|
}
|
|
for _, body := range [][]byte{multiMode0.bytes(), multiMode1.bytes()} {
|
|
if err := s.sendGameUpperRawClass(
|
|
session, uint16(dnfenum.CmdPacketSetUDPIPPort), body, 0); err != nil {
|
|
s.logGameEvent(session, "game-companion-real-actor-failed",
|
|
"stage", "multi-row-send", "error", err)
|
|
return false
|
|
}
|
|
}
|
|
session.townMu.Lock()
|
|
position := session.townPositionSnapshot
|
|
session.townMu.Unlock()
|
|
stateBits := currentActorDefaultUserStateBits
|
|
if runtime.Character.Stats != nil {
|
|
if value, exists := runtime.Character.Stats["user_state_bits"]; exists && value >= 0 && value <= 0xff {
|
|
stateBits = byte(value)
|
|
}
|
|
}
|
|
for index, row := range rows {
|
|
x := int(position.PositionX) + index*40
|
|
y := int(position.PositionY)
|
|
if !position.PositionValid {
|
|
x = int(numericCharacterStatValue(runtime.Character, "pos_x")) + index*40
|
|
y = int(numericCharacterStatValue(runtime.Character, "pos_y"))
|
|
}
|
|
if x < 0 {
|
|
x = 0
|
|
}
|
|
if x > 0xffff {
|
|
x = 0xffff
|
|
}
|
|
if y < 0 {
|
|
y = 0
|
|
}
|
|
if y > 0xffff {
|
|
y = 0xffff
|
|
}
|
|
areaBody := buildCurrentTownUserAreaNotificationBody(
|
|
currentSceneActorObjectKey(row.id),
|
|
position.TownID, 0xff, uint16(x), uint16(y),
|
|
position.MovementCode, stateBits,
|
|
)
|
|
if err := s.sendGameUpperRawClass(
|
|
session, currentTownUserAreaNotificationMsgID, areaBody, 0); err != nil {
|
|
s.logGameEvent(session, "game-companion-real-actor-failed",
|
|
"source_character_id", row.id,
|
|
"stage", "op23", "error", err)
|
|
return false
|
|
}
|
|
s.logGameEvent(session, "game-companion-real-actor-sent",
|
|
"source_character_id", row.id,
|
|
"name", row.name,
|
|
"owner_context", ownerContext,
|
|
"object_key", currentSceneActorObjectKey(row.id),
|
|
"mode0_bytes", len(row.mode0),
|
|
"mode1_bytes", len(row.mode1),
|
|
"path", currentCompanionRealActorsMode0Kind)
|
|
}
|
|
sent := len(rows)
|
|
if sent == 0 {
|
|
return false
|
|
}
|
|
s.logGameEvent(session, "game-companion-real-actors-published",
|
|
"owner_character_id", session.selectedCharacterID,
|
|
"count", sent,
|
|
"party_owner_context", ownerContext,
|
|
"aic", "pending_client_attachment")
|
|
return true
|
|
}
|