package protocol import ( "fmt" "erupe-ce/common/byteframe" "erupe-ce/common/stringsupport" "erupe-ce/cmd/protbot/conn" ) // SignResult holds the parsed response from a successful DSGN sign-in. type SignResult struct { TokenID uint32 TokenString string // 16 raw bytes as string Timestamp uint32 EntranceAddr string CharIDs []uint32 } // DoSign connects to the sign server and performs a DSGN login. // Reference: Erupe server/signserver/session.go (handleDSGN) and dsgn_resp.go (makeSignResponse). func DoSign(addr, username, password string) (*SignResult, error) { c, err := conn.DialWithInit(addr) if err != nil { return nil, fmt.Errorf("sign connect: %w", err) } defer func() { _ = c.Close() }() // Build DSGN request: "DSGN:041" + \x00 + SJIS(user) + \x00 + SJIS(pass) + \x00 + \x00 // The server reads: null-terminated request type, null-terminated user, null-terminated pass, null-terminated unk. // The request type has a 3-char version suffix (e.g. "041" for ZZ client mode 41) that the server strips. bf := byteframe.NewByteFrame() bf.WriteNullTerminatedBytes([]byte("DSGN:041")) // reqType with version suffix (server strips last 3 chars to get "DSGN:") bf.WriteNullTerminatedBytes(stringsupport.UTF8ToSJIS(username)) bf.WriteNullTerminatedBytes(stringsupport.UTF8ToSJIS(password)) bf.WriteUint8(0) // Unk null-terminated empty string if err := c.SendPacket(bf.Data()); err != nil { return nil, fmt.Errorf("sign send: %w", err) } resp, err := c.ReadPacket() if err != nil { return nil, fmt.Errorf("sign recv: %w", err) } return parseSignResponse(resp) } // parseSignResponse parses the binary response from the sign server. // Reference: Erupe server/signserver/dsgn_resp.go:makeSignResponse func parseSignResponse(data []byte) (*SignResult, error) { if len(data) < 1 { return nil, fmt.Errorf("empty sign response") } rbf := byteframe.NewByteFrameFromBytes(data) resultCode := rbf.ReadUint8() if resultCode != 1 { // SIGN_SUCCESS = 1 return nil, fmt.Errorf("sign failed with code %d", resultCode) } patchCount := rbf.ReadUint8() // patch server count (usually 2) _ = rbf.ReadUint8() // entrance server count (usually 1) charCount := rbf.ReadUint8() // character count result := &SignResult{} result.TokenID = rbf.ReadUint32() result.TokenString = string(rbf.ReadBytes(16)) // 16 raw bytes result.Timestamp = rbf.ReadUint32() // Skip patch server URLs (pascal strings with uint8 length prefix) for i := uint8(0); i < patchCount; i++ { strLen := rbf.ReadUint8() _ = rbf.ReadBytes(uint(strLen)) } // Read entrance server address (pascal string with uint8 length prefix) entranceLen := rbf.ReadUint8() result.EntranceAddr = string(rbf.ReadBytes(uint(entranceLen - 1))) _ = rbf.ReadUint8() // null terminator // Read character entries for i := uint8(0); i < charCount; i++ { charID := rbf.ReadUint32() result.CharIDs = append(result.CharIDs, charID) _ = rbf.ReadUint16() // HR _ = rbf.ReadUint16() // WeaponType _ = rbf.ReadUint32() // LastLogin _ = rbf.ReadUint8() // IsFemale _ = rbf.ReadUint8() // IsNewCharacter _ = rbf.ReadUint8() // Old GR _ = rbf.ReadUint8() // Use uint16 GR flag _ = rbf.ReadBytes(16) // Character name (padded) _ = rbf.ReadBytes(32) // Unk desc string (padded) // ZZ mode: additional fields _ = rbf.ReadUint16() // GR _ = rbf.ReadUint8() // Unk _ = rbf.ReadUint8() // Unk } return result, nil }