diff --git a/class_info.go b/class_info.go index 92c06b7..21fd0ec 100644 --- a/class_info.go +++ b/class_info.go @@ -6,7 +6,10 @@ import ( "github.com/jordanorelli/hyperstone/dota" ) +// classInfo container contains info about entity classes found in a given +// replay type classInfo struct { + names map[int]string } func (c *classInfo) handle(m proto.Message) { @@ -17,5 +20,6 @@ func (c *classInfo) handle(m proto.Message) { for _, class := range v.GetClasses() { fmt.Printf("class-id: %d network-name: %s table-name: %s\n", class.GetClassId(), class.GetNetworkName(), class.GetTableName()) + c.names[int(class.GetClassId())] = class.GetNetworkName() } } diff --git a/dt/field.go b/dt/field.go new file mode 100644 index 0000000..d560760 --- /dev/null +++ b/dt/field.go @@ -0,0 +1,74 @@ +package dt + +import ( + "bytes" + "fmt" + + "github.com/jordanorelli/hyperstone/dota" +) + +type Field struct { + _type Symbol + name Symbol + sendNode Symbol + bits *int + low *float32 + high *float32 + flags *int32 + serializer *Symbol + serializerVersion *int32 + encoder *Symbol +} + +func (f Field) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "{type: %s name: %s send: %s", f._type, f.name, f.sendNode) + if f.bits != nil { + fmt.Fprintf(&buf, " bits: %d", *f.bits) + } + if f.low != nil { + fmt.Fprintf(&buf, " low: %f", *f.low) + } + if f.high != nil { + fmt.Fprintf(&buf, " high: %f", *f.high) + } + if f.flags != nil { + fmt.Fprintf(&buf, " flags: %d", *f.flags) + } + if f.serializer != nil { + fmt.Fprintf(&buf, " serializer: %s", *f.serializer) + } + if f.serializerVersion != nil { + fmt.Fprintf(&buf, " serializer_v: %d", *f.serializerVersion) + } + if f.encoder != nil { + fmt.Fprintf(&buf, " encoder: %s", *f.encoder) + } + fmt.Fprint(&buf, "}") + return buf.String() +} + +func (f *Field) fromProto(flat *dota.ProtoFlattenedSerializerFieldT, t *SymbolTable) { + f._type = t.Symbol(int(flat.GetVarTypeSym())) + f.name = t.Symbol(int(flat.GetVarNameSym())) + if flat.BitCount == nil { + f.bits = nil + } else { + f.bits = new(int) + *f.bits = int(flat.GetBitCount()) + } + f.low = flat.LowValue + f.high = flat.HighValue + f.flags = flat.EncodeFlags + + if flat.FieldSerializerNameSym == nil { + f.serializer = nil + } else { + f.serializer = new(Symbol) + *f.serializer = t.Symbol(int(flat.GetFieldSerializerNameSym())) + } + + f.serializerVersion = flat.FieldSerializerVersion + // panic if we don't have a send node cause that shit is corrupt yo + f.sendNode = t.Symbol(int(*flat.SendNodeSym)) +} diff --git a/dt/serializer.go b/dt/serializer.go new file mode 100644 index 0000000..eecb042 --- /dev/null +++ b/dt/serializer.go @@ -0,0 +1,20 @@ +package dt + +import ( + "github.com/jordanorelli/hyperstone/dota" +) + +type Serializer struct { + Name Symbol + Version int + Fields []*Field +} + +func (s *Serializer) fromProto(v *dota.ProtoFlattenedSerializerT, st *SymbolTable, fields []Field) { + s.Name = st.Symbol(int(v.GetSerializerNameSym())) + s.Version = int(v.GetSerializerVersion()) + s.Fields = make([]*Field, len(v.GetFieldsIndex())) + for i, fi := range v.GetFieldsIndex() { + s.Fields[i] = &fields[fi] + } +} diff --git a/dt/symbol.go b/dt/symbol.go new file mode 100644 index 0000000..67e1731 --- /dev/null +++ b/dt/symbol.go @@ -0,0 +1,17 @@ +package dt + +// the internal representation of table data refers to all labels as +// interned strings (symbols). This array of string contains the mapping of +// symbol ids to symbol display representations. The sample replay I have +// at the time of writing this contains 2215 symbols in its symbol table. +// The dota replay format uses an ordered list of symbols. +type SymbolTable []string + +func (t *SymbolTable) Symbol(id int) Symbol { return Symbol{id: id, table: t} } + +type Symbol struct { + id int + table *SymbolTable +} + +func (s Symbol) String() string { return (*s.table)[s.id] } diff --git a/dt/tables.go b/dt/tables.go new file mode 100644 index 0000000..b436b93 --- /dev/null +++ b/dt/tables.go @@ -0,0 +1,56 @@ +package dt + +import ( + "fmt" + "io" + + "github.com/jordanorelli/hyperstone/dota" +) + +// TableSet represents a collection of tables. +type TableSet struct { + SymbolTable + Fields []Field + Serializers []Serializer +} + +func (t *TableSet) DebugPrint(w io.Writer) { + fmt.Fprintln(w, "Symbols:") + for _, sym := range t.SymbolTable { + fmt.Fprintf(w, "\t%s\n", sym) + } + fmt.Fprintln(w, "Fields:") + for _, f := range t.Fields { + fmt.Fprintf(w, "\t%s\n", f) + } + fmt.Fprintln(w, "Serializers:") + for _, s := range t.Serializers { + fmt.Fprintf(w, "\t%s (%d):\n", s.Name, s.Version) + for _, f := range s.Fields { + fmt.Fprintf(w, "\t\t%s\n", f) + } + } +} + +// ParseFlattened parses a flattened TableSet definition, as defined by the +// Dota replay protobufs. +func ParseFlattened(m *dota.CSVCMsg_FlattenedSerializer) *TableSet { + ts := &TableSet{SymbolTable: SymbolTable(m.GetSymbols())} + ts.parseFields(m.GetFields()) + ts.parseSerializers(m.GetSerializers()) + return ts +} + +func (ts *TableSet) parseFields(flat []*dota.ProtoFlattenedSerializerFieldT) { + ts.Fields = make([]Field, len(flat)) + for i, f := range flat { + ts.Fields[i].fromProto(f, &ts.SymbolTable) + } +} + +func (ts *TableSet) parseSerializers(flat []*dota.ProtoFlattenedSerializerT) { + ts.Serializers = make([]Serializer, len(flat)) + for i, s := range flat { + ts.Serializers[i].fromProto(s, &ts.SymbolTable, ts.Fields) + } +} diff --git a/entities.go b/entities.go new file mode 100644 index 0000000..5d35f65 --- /dev/null +++ b/entities.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "github.com/jordanorelli/hyperstone/dota" +) + +// type CSVCMsg_PacketEntities struct { +// MaxEntries *int32 +// UpdatedEntries *int32 +// IsDelta *bool +// UpdateBaseline *bool +// Baseline *int32 +// DeltaFrom *int32 +// EntityData []byte +// PendingFullFrame *bool +// ActiveSpawngroupHandle *uint32 +// MaxSpawngroupCreationsequence *uint32 +// } + +func dumpEntities(m proto.Message) { + switch v := m.(type) { + case *dota.CSVCMsg_PacketEntities: + data := v.GetEntityData() + if len(data) > 32 { + data = data[:32] + } + fmt.Printf("{MaxEntries: %d UpdatedEntries: %v IsDelta: %t UpdateBaseline: %t Baseline: %d DeltaFrom: %d EntityData: %x PendingFullFrame: %t ActiveSpawngroupHandle: %d}\n", v.GetMaxEntries(), v.GetUpdatedEntries(), v.GetIsDelta(), v.GetUpdateBaseline(), v.GetBaseline(), v.GetDeltaFrom(), data, v.GetPendingFullFrame(), v.GetActiveSpawngroupHandle()) + } +} diff --git a/main.go b/main.go index 6fbbad0..20c394a 100644 --- a/main.go +++ b/main.go @@ -124,12 +124,16 @@ func main() { handle = printTypes case "pretty": handle = prettyPrint + case "send-tables": + handle = sendTables case "string-tables": st := newStringTables() handle = st.handle case "class-info": ci := new(classInfo) handle = ci.handle + case "entities": + handle = dumpEntities default: bail(1, "no such action: %s", flag.Arg(0)) } diff --git a/send_tables.go b/send_tables.go new file mode 100644 index 0000000..c7013ab --- /dev/null +++ b/send_tables.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "os" + + "github.com/golang/protobuf/proto" + + "github.com/jordanorelli/hyperstone/bit" + "github.com/jordanorelli/hyperstone/dota" + "github.com/jordanorelli/hyperstone/dt" +) + +func sendTables(m proto.Message) { + v, ok := m.(*dota.CDemoSendTables) + if !ok { + return + } + + // sendtables only has one field, a binary data field. + data := v.GetData() + br := bit.NewBytesReader(data) + + // body is length-prefixed + size := int(bit.ReadVarInt(br)) + + buf := make([]byte, size) + br.Read(buf) + + serializer := dota.CSVCMsg_FlattenedSerializer{} + if err := proto.Unmarshal(buf, &serializer); err != nil { + fmt.Printf("error: %v\n", err) + return + } + + ts := dt.ParseFlattened(&serializer) + ts.DebugPrint(os.Stdout) +} diff --git a/string_tables.go b/string_tables.go index 13d78bd..6e121a3 100644 --- a/string_tables.go +++ b/string_tables.go @@ -2,11 +2,13 @@ package main import ( "fmt" + "os" + "unicode/utf8" + "github.com/golang/protobuf/proto" "github.com/golang/snappy" "github.com/jordanorelli/hyperstone/bit" "github.com/jordanorelli/hyperstone/dota" - "os" ) const ( @@ -101,10 +103,15 @@ func (s stringTableEntry) String() string { if s.value == nil { return fmt.Sprintf("{%s nil}", s.key) } + + if utf8.Valid(s.value) { + return fmt.Sprintf("{%s %s}", s.key, s.value) + } + if len(s.value) > 32 { - return fmt.Sprintf("{%s %x}", s.key, s.value[:32]) + return fmt.Sprintf("{%s 0x%x}", s.key, s.value[:32]) } - return fmt.Sprintf("{%s %x}", s.key, s.value) + return fmt.Sprintf("{%s 0x%x}", s.key, s.value) } func (s *stringTables) handle(m proto.Message) {