package dnfbridge import ( "bytes" "encoding/binary" "net" "testing" "time" "longheng.io/server/internal/modules/dnf/channelcatalog" ) type companionPartyTestConn struct { net.Conn write bytes.Buffer } func (c *companionPartyTestConn) Write(data []byte) (int, error) { return c.write.Write(data) } func (c *companionPartyTestConn) SetWriteDeadline(time.Time) error { return nil } func TestCompanionPresenceFriendRowRendersOnline(t *testing.T) { service := &Service{} virtual := &gameSession{ selectedCharacterID: 6, characterGeneration: 7, channel: channelcatalog.Channel{ID: 11}, companionVirtual: true, companionOwnerID: 2, } service.companionSessions = map[uint16]*gameSession{6: virtual} presence := service.currentUnitedFriendPresence(6) if presence.Busy { t.Fatal("volunteer friend row must not render busy/登录中") } if presence.ChannelID != 11 { t.Fatalf("friend row channel=%d want 11", presence.ChannelID) } } func TestBuildCompanionPartyStateUsesNativeFourSlotRoster(t *testing.T) { service := &Service{} owner := &gameSession{selectedCharacterID: 2} run := ¤tDungeonCompanionRun{ Ready: true, Sources: []currentDungeonCompanionSourceRun{ {Ready: true, SourceCharacterID: 6}, {Ready: false, SourceCharacterID: 7}, {Ready: true, SourceCharacterID: 3}, }, } state := service.buildCompanionPartyState(owner, run) if state.PartyID <= 0 || state.PartyID >= 0xFFFF { t.Fatalf("party id=%d is not a printable native party", state.PartyID) } if state.UserID != 2 || !state.IsLeader || state.MaxMembers != 4 { t.Fatalf("party identity=%+v", state) } if len(state.Members) != 3 { t.Fatalf("members=%v want owner + 2 ready volunteers", state.Members) } want := []struct { id uint16 slot byte }{{2, 0}, {6, 1}, {3, 2}} for index, expected := range want { member := state.Members[index] if member.UserID != expected.id || member.Slot != expected.slot { t.Fatalf("member %d=%+v want id=%d slot=%d", index, member, expected.id, expected.slot) } if member.HPPercent != 100 || member.MPPercent != 100 { t.Fatalf("member %d hp/mp=%d/%d want 100/100", index, member.HPPercent, member.MPPercent) } } } func TestClearCompanionNativePartySendsOp9ThenOp153(t *testing.T) { connection := &companionPartyTestConn{} session := &gameSession{ conn: connection, connID: "companion-party-clear", selectedCharacterID: 2, characterGeneration: 3, channel: channelcatalog.Channel{ID: 11}, } run := ¤tDungeonCompanionRun{ Ready: true, NativePartyID: 0xE002, NativePartySent: true, } service := &Service{} service.clearCurrentDungeonCompanionNativeParty(session, run) if run.NativePartySent { t.Fatal("native party flag was not cleared") } stream := connection.write.Bytes() headerSize := service.gameUpperHeaderSize() msgIDs := make([]uint16, 0, 2) for len(stream) > 0 { if len(stream) < headerSize { t.Fatalf("truncated packet header: %d", len(stream)) } total := int(binary.LittleEndian.Uint32(stream[3:7])) if total < headerSize || total > len(stream) { t.Fatalf("packet total=%d stream=%d", total, len(stream)) } msgIDs = append(msgIDs, binary.LittleEndian.Uint16(stream[1:3])) stream = stream[total:] } if len(msgIDs) != 2 || msgIDs[0] != 9 || msgIDs[1] != 0x0099 { t.Fatalf("clear packets=%v want [9 153]", msgIDs) } } func TestCompanionSessionIdentityIsCurrentOnlyWhileRegistered(t *testing.T) { service := &Service{} virtual := &gameSession{selectedCharacterID: 6, characterGeneration: 9, companionVirtual: true} service.companionSessions = map[uint16]*gameSession{6: virtual} identity, ok := service.companionSessionSnapshot(6) if !ok || !identity.virtual || !service.boundGameSessionCharacterCurrent(identity) { t.Fatalf("virtual identity=%+v ok=%v current=%v", identity, ok, service.boundGameSessionCharacterCurrent(identity)) } delete(service.companionSessions, 6) if service.boundGameSessionCharacterCurrent(identity) { t.Fatal("removed volunteer session must fail the liveness check") } }