475 lines
12 KiB
Go
475 lines
12 KiB
Go
package cps
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
const testCSS = `
|
|
CreateCharacter {
|
|
.v1 {
|
|
account_id: int32;
|
|
server_id: int32;
|
|
name: string16;
|
|
race: int8;
|
|
gender: int8;
|
|
deity: int8;
|
|
class: int8;
|
|
level: int8;
|
|
starting_zone: int8;
|
|
unknown1: int8[2];
|
|
race_file: string16;
|
|
skin_color: float[3];
|
|
eye_color: float[3];
|
|
hair_color1: float[3];
|
|
hair_color2: float[3];
|
|
hair_highlight: float[3];
|
|
unknown2: int8[26];
|
|
hair_file: string16;
|
|
hair_type_color: float[3];
|
|
hair_type_highlight_color: float[3];
|
|
face_file: string16;
|
|
hair_face_color: float[3];
|
|
hair_face_highlight_color: float[3];
|
|
chest_file: string16;
|
|
shirt_color: float[3];
|
|
unknown_chest_color: float[3];
|
|
legs_file: string16;
|
|
pants_color: float[3];
|
|
unknown_legs_color: float[3];
|
|
unknown9: float[3];
|
|
eyes2: float[3];
|
|
ears: float[3];
|
|
eye_brows: float[3];
|
|
cheeks: float[3];
|
|
lips: float[3];
|
|
chin: float[3];
|
|
nose: float[3];
|
|
body_size: float;
|
|
body_age: float;
|
|
}
|
|
|
|
.v373 {
|
|
unknown0: int32 @0; /* Insert at beginning */
|
|
}
|
|
|
|
.v562 {
|
|
unknown0: int8; /* Override type from v373 */
|
|
unknown1: int32 @1; /* New field after unknown0 */
|
|
unknown3: int8 @3; /* Insert at position 3 */
|
|
|
|
/* Override color types */
|
|
skin_color: color;
|
|
skin_color2: color @after(skin_color);
|
|
eye_color: color;
|
|
hair_color1: color;
|
|
hair_color2: color;
|
|
hair_highlight: color;
|
|
hair_type_color: color;
|
|
hair_type_highlight_color: color;
|
|
hair_face_color: color;
|
|
hair_face_highlight_color: color;
|
|
shirt_color: color;
|
|
unknown_chest_color: color;
|
|
pants_color: color;
|
|
unknown_legs_color: color;
|
|
unknown9: color;
|
|
|
|
/* Add wing support */
|
|
wing_file: string16 @after(hair_face_highlight_color);
|
|
wing_color1: color;
|
|
wing_color2: color;
|
|
|
|
/* Add SOGA system */
|
|
soga_version: int8;
|
|
soga_race_file: string16;
|
|
soga_skin_color: color;
|
|
soga_eye_color: color;
|
|
soga_hair_color1: color;
|
|
soga_hair_color2: color;
|
|
soga_hair_highlight: color;
|
|
soga_unknown11: int8[26];
|
|
soga_hair_file: string16;
|
|
soga_hair_type_color: color;
|
|
soga_hair_type_highlight_color: color;
|
|
soga_face_file: string16;
|
|
soga_hair_face_color: color;
|
|
soga_hair_face_highlight_color: color;
|
|
soga_wing_file: string16;
|
|
soga_wing_color1: color;
|
|
soga_wing_color2: color;
|
|
soga_chest_file: string16;
|
|
soga_shirt_color: color;
|
|
soga_unknown_chest_color: color;
|
|
soga_legs_file: string16;
|
|
soga_pants_color: color;
|
|
soga_unknown_legs_color: color;
|
|
soga_unknown12: color;
|
|
soga_eyes2: float[3];
|
|
soga_ears: float[3];
|
|
soga_eye_brows: float[3];
|
|
soga_cheeks: float[3];
|
|
soga_lips: float[3];
|
|
soga_chin: float[3];
|
|
soga_nose: float[3];
|
|
soga_body_size: float;
|
|
soga_body_age: float;
|
|
}
|
|
|
|
.v57080 {
|
|
unknown10: int16 @after(starting_zone);
|
|
unknown_skin_color2: color @after(eye_color);
|
|
/* Remove skin_color2 positioning */
|
|
skin_color2: none;
|
|
}
|
|
}
|
|
|
|
BadLanguageFilter {
|
|
.v1 {
|
|
num_words: int16;
|
|
words_array: string16[];
|
|
}
|
|
}`
|
|
|
|
func TestParser(t *testing.T) {
|
|
parser := NewParser(testCSS)
|
|
packets, err := parser.Parse()
|
|
if err != nil {
|
|
t.Fatalf("Parse failed: %v", err)
|
|
}
|
|
|
|
// Verify CreateCharacter packet exists
|
|
createChar, exists := packets["CreateCharacter"]
|
|
if !exists {
|
|
t.Fatal("CreateCharacter packet not found")
|
|
}
|
|
|
|
// Verify versions
|
|
expectedVersions := []int{1, 373, 562, 57080}
|
|
for _, version := range expectedVersions {
|
|
if _, exists := createChar.Versions[version]; !exists {
|
|
t.Errorf("Version %d not found", version)
|
|
}
|
|
}
|
|
|
|
// Verify v1 has expected fields
|
|
v1 := createChar.Versions[1]
|
|
expectedV1Fields := []string{"account_id", "name", "race", "skin_color"}
|
|
for _, fieldName := range expectedV1Fields {
|
|
if _, exists := v1.Fields[fieldName]; !exists {
|
|
t.Errorf("v1 missing field: %s", fieldName)
|
|
}
|
|
}
|
|
|
|
// Verify field types
|
|
if v1.Fields["account_id"].Type != TypeInt32 {
|
|
t.Error("account_id should be int32")
|
|
}
|
|
if v1.Fields["name"].Type != TypeString16 {
|
|
t.Error("name should be string16")
|
|
}
|
|
if v1.Fields["skin_color"].Type != TypeFloat32 || v1.Fields["skin_color"].Size != 3 {
|
|
t.Error("skin_color should be float[3]")
|
|
}
|
|
|
|
// Verify v373 override
|
|
v373 := createChar.Versions[373]
|
|
if v373.Fields["unknown0"].Type != TypeInt32 || v373.Fields["unknown0"].Position != 0 {
|
|
t.Error("v373 unknown0 should be int32 @0")
|
|
}
|
|
|
|
// Verify v562 type override
|
|
v562 := createChar.Versions[562]
|
|
if v562.Fields["unknown0"].Type != TypeInt8 {
|
|
t.Error("v562 should override unknown0 to int8")
|
|
}
|
|
if v562.Fields["skin_color"].Type != TypeColor {
|
|
t.Error("v562 should override skin_color to color")
|
|
}
|
|
|
|
// Verify positioning
|
|
if v562.Fields["unknown1"].Position != 1 {
|
|
t.Error("unknown1 should have position 1")
|
|
}
|
|
if v562.Fields["skin_color2"].After != "skin_color" {
|
|
t.Error("skin_color2 should be after skin_color")
|
|
}
|
|
|
|
// Verify removal
|
|
v57080 := createChar.Versions[57080]
|
|
if !v57080.Fields["skin_color2"].Remove {
|
|
t.Error("skin_color2 should be removed in v57080")
|
|
}
|
|
}
|
|
|
|
func TestCompiler(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
|
|
// Test v1 compilation
|
|
packet, err := manager.GetPacket("CreateCharacter", 1)
|
|
if err != nil {
|
|
t.Fatalf("Failed to compile v1: %v", err)
|
|
}
|
|
|
|
if packet.Name != "CreateCharacter" || packet.Version != 1 {
|
|
t.Error("Incorrect packet name or version")
|
|
}
|
|
|
|
// Verify field order and types
|
|
if len(packet.Fields) == 0 {
|
|
t.Fatal("No fields in compiled packet")
|
|
}
|
|
|
|
// First field should be account_id
|
|
if packet.Fields[0].Name != "account_id" || packet.Fields[0].Type != TypeInt32 {
|
|
t.Error("First field should be account_id:int32")
|
|
}
|
|
|
|
// Test v373 compilation (should inherit v1 + unknown0 at beginning)
|
|
packet373, err := manager.GetPacket("CreateCharacter", 373)
|
|
if err != nil {
|
|
t.Fatalf("Failed to compile v373: %v", err)
|
|
}
|
|
|
|
// Should have more fields than v1
|
|
if len(packet373.Fields) <= len(packet.Fields) {
|
|
t.Error("v373 should have more fields than v1")
|
|
}
|
|
|
|
// First field should be unknown0
|
|
if packet373.Fields[0].Name != "unknown0" || packet373.Fields[0].Type != TypeInt32 {
|
|
t.Error("v373 first field should be unknown0:int32")
|
|
}
|
|
|
|
// Test v562 compilation (type overrides)
|
|
packet562, err := manager.GetPacket("CreateCharacter", 562)
|
|
if err != nil {
|
|
t.Fatalf("Failed to compile v562: %v", err)
|
|
}
|
|
|
|
// Find skin_color field and verify it's now color type
|
|
var skinColorField *CompiledField
|
|
for _, field := range packet562.Fields {
|
|
if field.Name == "skin_color" {
|
|
skinColorField = field
|
|
break
|
|
}
|
|
}
|
|
if skinColorField == nil || skinColorField.Type != TypeColor {
|
|
t.Error("v562 skin_color should be color type")
|
|
}
|
|
|
|
// Verify unknown0 type override
|
|
if packet562.Fields[0].Type != TypeInt8 {
|
|
t.Error("v562 unknown0 should be int8 (overridden from int32)")
|
|
}
|
|
|
|
// Test field removal in v57080
|
|
packet57080, err := manager.GetPacket("CreateCharacter", 57080)
|
|
if err != nil {
|
|
t.Fatalf("Failed to compile v57080: %v", err)
|
|
}
|
|
|
|
// skin_color2 should not exist
|
|
for _, field := range packet57080.Fields {
|
|
if field.Name == "skin_color2" {
|
|
t.Error("skin_color2 should be removed in v57080")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVersionSelection(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
|
|
// Test exact version match
|
|
packet, err := manager.GetPacket("CreateCharacter", 562)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get v562: %v", err)
|
|
}
|
|
if packet.Version != 562 {
|
|
t.Error("Should get exact version match")
|
|
}
|
|
|
|
// Test version fallback (should get highest <= requested)
|
|
packet, err = manager.GetPacket("CreateCharacter", 500)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get v500 fallback: %v", err)
|
|
}
|
|
if packet.Version != 373 {
|
|
t.Error("Should fallback to v373 for client v500")
|
|
}
|
|
|
|
// Test future version (should get highest available)
|
|
packet, err = manager.GetPacket("CreateCharacter", 99999)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get future version: %v", err)
|
|
}
|
|
if packet.Version != 57080 {
|
|
t.Error("Should get highest version for future client")
|
|
}
|
|
|
|
// Test too old client
|
|
_, err = manager.GetPacket("CreateCharacter", 0)
|
|
if err == nil {
|
|
t.Error("Should fail for client version 0")
|
|
}
|
|
}
|
|
|
|
func TestSerialization(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
serializer := NewSerializer(manager)
|
|
|
|
// Test v1 serialization
|
|
data := map[string]any{
|
|
"account_id": int32(12345),
|
|
"server_id": int32(1),
|
|
"name": "TestChar",
|
|
"race": int8(1),
|
|
"gender": int8(1),
|
|
"deity": int8(0),
|
|
"class": int8(1),
|
|
"level": int8(1),
|
|
"skin_color": []float32{1.0, 0.8, 0.6},
|
|
}
|
|
|
|
bytes, err := serializer.Serialize("CreateCharacter", 1, data)
|
|
if err != nil {
|
|
t.Fatalf("Serialization failed: %v", err)
|
|
}
|
|
|
|
if len(bytes) == 0 {
|
|
t.Fatal("Serialized data is empty")
|
|
}
|
|
|
|
// Test deserialization
|
|
parsed, err := serializer.Deserialize("CreateCharacter", 1, bytes)
|
|
if err != nil {
|
|
t.Fatalf("Deserialization failed: %v", err)
|
|
}
|
|
|
|
// Verify key fields
|
|
if parsed["account_id"].(int32) != 12345 {
|
|
t.Error("account_id not preserved")
|
|
}
|
|
if parsed["name"].(string) != "TestChar" {
|
|
t.Error("name not preserved")
|
|
}
|
|
if parsed["race"].(int8) != 1 {
|
|
t.Error("race not preserved")
|
|
}
|
|
|
|
// Verify float array
|
|
skinColor := parsed["skin_color"].([]float32)
|
|
if len(skinColor) != 3 || skinColor[0] != 1.0 {
|
|
t.Error("skin_color array not preserved")
|
|
}
|
|
}
|
|
|
|
func TestColorSerialization(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
serializer := NewSerializer(manager)
|
|
|
|
// Test v562 with color types
|
|
data := map[string]any{
|
|
"account_id": int32(12345),
|
|
"name": "TestChar",
|
|
"skin_color": uint32(0xFF0000FF), // Red color
|
|
"eye_color": uint32(0x0000FFFF), // Blue color
|
|
"wing_color1": uint32(0x00FF00FF), // Green color
|
|
}
|
|
|
|
bytes, err := serializer.Serialize("CreateCharacter", 562, data)
|
|
if err != nil {
|
|
t.Fatalf("v562 serialization failed: %v", err)
|
|
}
|
|
|
|
parsed, err := serializer.Deserialize("CreateCharacter", 562, bytes)
|
|
if err != nil {
|
|
t.Fatalf("v562 deserialization failed: %v", err)
|
|
}
|
|
|
|
// Verify colors preserved
|
|
if parsed["skin_color"].(uint32) != 0xFF0000FF {
|
|
t.Error("skin_color not preserved")
|
|
}
|
|
if parsed["eye_color"].(uint32) != 0x0000FFFF {
|
|
t.Error("eye_color not preserved")
|
|
}
|
|
}
|
|
|
|
func TestBadLanguageFilter(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
|
|
packet, err := manager.GetPacket("BadLanguageFilter", 1)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get BadLanguageFilter: %v", err)
|
|
}
|
|
|
|
if len(packet.Fields) != 2 {
|
|
t.Error("BadLanguageFilter should have 2 fields")
|
|
}
|
|
|
|
// Verify dynamic array
|
|
wordsField := packet.Fields[1]
|
|
if wordsField.Name != "words_array" || !wordsField.Dynamic {
|
|
t.Error("words_array should be dynamic")
|
|
}
|
|
}
|
|
|
|
func TestCaching(t *testing.T) {
|
|
manager := MustLoadCSS(testCSS)
|
|
|
|
// Get same packet twice
|
|
packet1, err := manager.GetPacket("CreateCharacter", 562)
|
|
if err != nil {
|
|
t.Fatalf("First get failed: %v", err)
|
|
}
|
|
|
|
packet2, err := manager.GetPacket("CreateCharacter", 562)
|
|
if err != nil {
|
|
t.Fatalf("Second get failed: %v", err)
|
|
}
|
|
|
|
// Should be same pointer (cached)
|
|
if packet1 != packet2 {
|
|
t.Error("Packets should be cached and identical")
|
|
}
|
|
|
|
// Clear cache and verify new instance
|
|
manager.ClearCache()
|
|
packet3, err := manager.GetPacket("CreateCharacter", 562)
|
|
if err != nil {
|
|
t.Fatalf("Third get failed: %v", err)
|
|
}
|
|
|
|
if packet1 == packet3 {
|
|
t.Error("After cache clear, should get new instance")
|
|
}
|
|
}
|
|
|
|
func TestErrorCases(t *testing.T) {
|
|
// Test invalid CSS
|
|
invalidCSS := `CreateCharacter { .v1 { invalid syntax } }`
|
|
manager := NewManager()
|
|
err := manager.LoadCSS(invalidCSS)
|
|
if err == nil {
|
|
t.Error("Should fail on invalid CSS")
|
|
}
|
|
|
|
// Test missing packet
|
|
manager = MustLoadCSS(testCSS)
|
|
_, err = manager.GetPacket("NonExistent", 1)
|
|
if err == nil {
|
|
t.Error("Should fail on missing packet")
|
|
}
|
|
|
|
// Test serialization with wrong data types
|
|
serializer := NewSerializer(manager)
|
|
badData := map[string]any{
|
|
"account_id": "not_a_number", // Wrong type
|
|
}
|
|
_, err = serializer.Serialize("CreateCharacter", 1, badData)
|
|
if err == nil {
|
|
t.Error("Should fail on wrong data type")
|
|
}
|
|
}
|