diff --git a/bit/decode.go b/bit/decode.go index d3ccf11..da4e3b7 100644 --- a/bit/decode.go +++ b/bit/decode.go @@ -1,5 +1,9 @@ package bit +import ( + "bytes" +) + // ReadUbitVar reads a prefixed uint value. A prefix is 2 bits wide, followed // by the 4 least-significant bits, then a variable number of most-significant // bits based on the prefix. @@ -45,3 +49,16 @@ func ReadVarInt(r Reader) uint64 { } return x } + +func ReadBool(r Reader) bool { + return r.ReadBits(1) != 0 +} + +// reads a null-terminated string +func ReadString(r Reader) string { + var buf bytes.Buffer + for b := r.ReadByte(); b != 0; b = r.ReadByte() { + buf.WriteByte(b) + } + return buf.String() +} diff --git a/main.go b/main.go index 36383e9..14b174c 100644 --- a/main.go +++ b/main.go @@ -109,25 +109,25 @@ func printTypes(m proto.Message) { } func prettySlice(v reflect.Value) string { - if v.Type().Elem().Kind() == reflect.Uint8 { - l := v.Len() - if l > 16 { - l = 16 - } - b := make([]byte, l) - for i := 0; i < l; i++ { - b[i] = byte(v.Index(i).Uint()) - } - return fmt.Sprintf("%x", b) - } - - width := 0 - parts := make([]string, 0, v.Len()) - for i := 0; i < v.Len() && width <= 32; i++ { - parts = append(parts, pretty(v.Index(i))) - width += len(parts[i]) // obligatory byte count is not rune count rabble - } - return fmt.Sprintf("[%s]", strings.Join(parts, ", ")) + if v.Type().Elem().Kind() == reflect.Uint8 { + l := v.Len() + if l > 16 { + l = 16 + } + b := make([]byte, l) + for i := 0; i < l; i++ { + b[i] = byte(v.Index(i).Uint()) + } + return fmt.Sprintf("%x", b) + } + + width := 0 + parts := make([]string, 0, v.Len()) + for i := 0; i < v.Len() && width <= 32; i++ { + parts = append(parts, pretty(v.Index(i))) + width += len(parts[i]) // obligatory byte count is not rune count rabble + } + return fmt.Sprintf("[%s]", strings.Join(parts, ", ")) } func prettyStruct(v reflect.Value) string { @@ -138,38 +138,38 @@ func prettyStruct(v reflect.Value) string { if field.Name == "XXX_unrecognized" { continue } - fv := v.Field(fn) - fmt.Fprintf(&buf, "%s: %s ", field.Name, pretty(fv)) + fv := v.Field(fn) + fmt.Fprintf(&buf, "%s: %s ", field.Name, pretty(fv)) } - fmt.Fprint(&buf, "}") - return buf.String() + fmt.Fprint(&buf, "}") + return buf.String() } func pretty(v reflect.Value) string { switch v.Kind() { - case reflect.Ptr: - if v.IsNil() { - return "nil" - } - return pretty(v.Elem()) + case reflect.Ptr: + if v.IsNil() { + return "nil" + } + return pretty(v.Elem()) case reflect.Struct: return prettyStruct(v) - case reflect.Slice: - return prettySlice(v) + case reflect.Slice: + return prettySlice(v) case reflect.String: - return fmt.Sprintf("%q", v.String()) + return fmt.Sprintf("%q", v.String()) case reflect.Int32: - return fmt.Sprintf("%d", v.Int()) + return fmt.Sprintf("%d", v.Int()) case reflect.Uint8, reflect.Uint32: - return fmt.Sprintf("%d", v.Uint()) + return fmt.Sprintf("%d", v.Uint()) case reflect.Bool: - return fmt.Sprintf("%t", v.Bool()) - default: - return v.Type().Name() + return fmt.Sprintf("%t", v.Bool()) + default: + return v.Type().Name() } } -func prettyVals(m proto.Message) { +func prettyPrint(m proto.Message) { v := reflect.ValueOf(m) fmt.Println(pretty(v)) } @@ -190,7 +190,10 @@ func main() { case "types": handle = printTypes case "pretty": - handle = prettyVals + handle = prettyPrint + case "string-tables": + st := newStringTables() + handle = st.handle default: bail(1, "no such action: %s", flag.Arg(0)) } diff --git a/string_tables.go b/string_tables.go new file mode 100644 index 0000000..4674e6b --- /dev/null +++ b/string_tables.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "github.com/golang/snappy" + "github.com/jordanorelli/hyperstone/bit" + "github.com/jordanorelli/hyperstone/dota" + "os" +) + +const ( + sTableRingSize = 32 +) + +type stringTables struct { + tables []stringTable + idx map[string]*stringTable + br *bit.BufReader + scratch []byte +} + +func (s *stringTables) String() string { + if s.scratch == nil { + return fmt.Sprintf("{%v %v %v nil}", s.tables, s.idx, s.br) + } + if len(s.scratch) > 32 { + return fmt.Sprintf("{%v %v %v %x...}", s.tables, s.idx, s.br, s.scratch[:32]) + } + return fmt.Sprintf("{%v %v %v %x}", s.tables, s.idx, s.br, s.scratch) +} + +func newStringTables() *stringTables { + return &stringTables{ + tables: make([]stringTable, 0, 64), + idx: make(map[string]*stringTable, 64), + br: new(bit.BufReader), + scratch: make([]byte, 1<<16), + } +} + +type stringTable []stringTableEntry + +func (t stringTable) create(br *bit.BufReader, byteSize, bitSize int) { + idx := -1 + for i, base := 0, uint64(0); i < len(t); i++ { + if i > 32 { + base++ + } + + // continue flag + if bit.ReadBool(br) { + idx++ + } else { + // in practice, the one replay I'm using never hits this branch, so + // I do not know if it works. The base referenced from above might + // be wrong in this branch. + idx = int(bit.ReadVarInt(br)) + } + + // key flag + if bit.ReadBool(br) { + // backreading flag + if bit.ReadBool(br) { + t[idx].key = t[base+br.ReadBits(5)].key[:br.ReadBits(5)] + bit.ReadString(br) + } else { + t[idx].key = bit.ReadString(br) + } + } + + // value flag + if bit.ReadBool(br) { + if byteSize != -1 { + t[idx].value = make([]byte, byteSize) + br.Read(t[idx].value) + } else { + size, _ := br.ReadBits(14), br.ReadBits(3) + t[idx].value = make([]byte, size) + br.Read(t[idx].value) + } + } + } +} + +type stringTableEntry struct { + key string + value []byte +} + +func (s stringTableEntry) String() string { + if s.value == nil { + return fmt.Sprintf("{%s nil}", s.key) + } + if len(s.value) > 32 { + return fmt.Sprintf("{%s %x}", s.key, s.value[:32]) + } + return fmt.Sprintf("{%s %x}", s.key, s.value) +} + +func (s *stringTables) handle(m proto.Message) { + switch v := m.(type) { + case *dota.CSVCMsg_CreateStringTable: + prettyPrint(m) + s.handleCreate(v) + fmt.Println(s) + case *dota.CSVCMsg_UpdateStringTable: + // prettyPrint(m) + case *dota.CSVCMsg_ClearAllStringTables: + // prettyPrint(m) + case *dota.CDemoStringTables: + // prettyPrint(m) + } +} + +// type CSVCMsg_CreateStringTable struct { +// Name *string +// NumEntries *int32 +// UserDataFixedSize *bool +// UserDataSize *int32 +// UserDataSizeBits *int32 +// Flags *int32 +// StringData []byte +// UncompressedSize *int32 +// DataCompressed *bool +// } + +func (s *stringTables) handleCreate(m *dota.CSVCMsg_CreateStringTable) { + fmt.Printf("create %s\n", m.GetName()) + s.tables = append(s.tables, make(stringTable, m.GetNumEntries())) + s.idx[m.GetName()] = &s.tables[len(s.tables)-1] + table := &s.tables[len(s.tables)-1] + + sd := m.GetStringData() + if sd == nil || len(sd) == 0 { + return + } + + if m.GetDataCompressed() { + switch string(sd[:4]) { + case "LZSS": + // TODO: not this + panic("no lzss support!") + default: + var err error + sd, err = snappy.Decode(s.scratch, sd) + if err != nil { + fmt.Fprintf(os.Stderr, "stringtable decode error: %v", err) + return + } + } + } + + s.br.SetSource(sd) + if m.GetUserDataFixedSize() { + table.create(s.br, int(m.GetUserDataSize()), int(m.GetUserDataSizeBits())) + } else { + table.create(s.br, -1, -1) + } +}