112 lines
3.9 KiB
Go
112 lines
3.9 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
|
|
)
|
|
|
|
// companionOfflineTestConn is intentionally local to this test so the file can
|
|
// be compiled without the large shared service_test.go harness.
|
|
type companionOfflineTestConn struct {
|
|
net.Conn
|
|
write bytes.Buffer
|
|
}
|
|
|
|
func (c *companionOfflineTestConn) Write(data []byte) (int, error) { return c.write.Write(data) }
|
|
func (c *companionOfflineTestConn) SetWriteDeadline(time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
func TestBuildCurrentDungeonCompanionOfflineBodyMatchesPresentZeroContract(t *testing.T) {
|
|
body := buildCurrentDungeonCompanionOfflineBody(12345)
|
|
if len(body) != currentDungeonCompanionProjectionHeaderSize {
|
|
t.Fatalf("body length=%d want=%d", len(body), currentDungeonCompanionProjectionHeaderSize)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[0:4]); got != currentDungeonCompanionProjectionMagic {
|
|
t.Fatalf("magic=%#x want=%#x", got, currentDungeonCompanionProjectionMagic)
|
|
}
|
|
if body[4] != currentDungeonCompanionProjectionVersion {
|
|
t.Fatalf("version=%d want=%d", body[4], currentDungeonCompanionProjectionVersion)
|
|
}
|
|
if body[5] != 0 {
|
|
t.Fatalf("flags=%#x want 0 (present must be clear)", body[5])
|
|
}
|
|
if got := binary.LittleEndian.Uint16(body[6:8]); got != 0 {
|
|
t.Fatalf("source_character=%d want 0", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint64(body[8:16]); got != 12345 {
|
|
t.Fatalf("revision=%d want 12345", got)
|
|
}
|
|
for offset := 16; offset < len(body); offset++ {
|
|
if body[offset] != 0 {
|
|
t.Fatalf("byte %d=%#x want 0", offset, body[offset])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSendCurrentDungeonCompanionTeamOfflineIsIdempotent(t *testing.T) {
|
|
connection := &companionOfflineTestConn{}
|
|
session := &gameSession{conn: connection, connID: "companion-team-logout"}
|
|
run := ¤tDungeonCompanionRun{
|
|
Ready: true,
|
|
TeamMode: true,
|
|
TeamLeaderID: 1,
|
|
TeamSize: 2,
|
|
Online: true,
|
|
Sources: []currentDungeonCompanionSourceRun{
|
|
{Ready: true, SourceCharacterID: 2, Body: buildCurrentDungeonCompanionOfflineBody(77)},
|
|
{Ready: true, SourceCharacterID: 3, Body: buildCurrentDungeonCompanionOfflineBody(77)},
|
|
},
|
|
}
|
|
runtime := &runtimeDungeonState{Companion: run}
|
|
service := &Service{}
|
|
|
|
service.sendCurrentDungeonCompanionTeamOffline(session, runtime, "dungeon_return_town")
|
|
if !run.LogoutSent || run.Online {
|
|
t.Fatalf("run state after logout = sent:%v online:%v", run.LogoutSent, run.Online)
|
|
}
|
|
if run.OfflineAt.IsZero() || run.OfflineReason != "dungeon_return_town" {
|
|
t.Fatalf("offline evidence = at:%v reason:%q", run.OfflineAt, run.OfflineReason)
|
|
}
|
|
first := connection.write.Len()
|
|
if first == 0 {
|
|
t.Fatal("logout packet was not sent")
|
|
}
|
|
|
|
// A second boundary must not replay the packet.
|
|
service.sendCurrentDungeonCompanionTeamOffline(session, runtime, "return_select_character")
|
|
if connection.write.Len() != first {
|
|
t.Fatalf("second logout wrote %d extra bytes", connection.write.Len()-first)
|
|
}
|
|
|
|
stream := connection.write.Bytes()
|
|
headerSize := service.gameUpperHeaderSize()
|
|
if len(stream) < headerSize {
|
|
t.Fatalf("logout stream too short: %d", len(stream))
|
|
}
|
|
total := int(binary.LittleEndian.Uint32(stream[3:7]))
|
|
if total != len(stream) {
|
|
t.Fatalf("logout packet total=%d stream=%d", total, len(stream))
|
|
}
|
|
if msgID := binary.LittleEndian.Uint16(stream[1:3]); msgID != currentDungeonCompanionProjectionOpcode {
|
|
t.Fatalf("msg id=%#x want=%#x", msgID, currentDungeonCompanionProjectionOpcode)
|
|
}
|
|
body := stream[headerSize:total]
|
|
if len(body) != currentDungeonCompanionProjectionHeaderSize || body[5] != 0 {
|
|
t.Fatalf("logout body length=%d flags=%d", len(body), body[5])
|
|
}
|
|
}
|
|
|
|
func TestMatchTeamAutoFillKeepsProjectionPipelinePerSource(t *testing.T) {
|
|
// The matcher is pure; this guard pins the cap the bridge passes so a future
|
|
// change cannot silently form a five-player dungeon party.
|
|
if dnfcompanion.TeamMaxCompanions != 3 {
|
|
t.Fatalf("TeamMaxCompanions=%d want 3", dnfcompanion.TeamMaxCompanions)
|
|
}
|
|
}
|