diff --git a/ent/class.go b/ent/class.go index 546c331..3a92049 100644 --- a/ent/class.go +++ b/ent/class.go @@ -10,6 +10,9 @@ type Class struct { Name Symbol Version int Fields []*Field + + // all other entities for this class use this instance as a prototype + baseline *Entity } func (c *Class) New() *Entity { diff --git a/ent/dict.go b/ent/dict.go index 24c1ce6..bf5033d 100644 --- a/ent/dict.go +++ b/ent/dict.go @@ -2,13 +2,29 @@ package ent import ( "fmt" + "strconv" "github.com/golang/protobuf/proto" "github.com/jordanorelli/hyperstone/bit" "github.com/jordanorelli/hyperstone/dota" + "github.com/jordanorelli/hyperstone/stbl" ) +// https://developer.valvesoftware.com/wiki/Entity_limit +// +// There can be up to 4096 entities. This total is split into two groups of 2048: +// Non-networked entities, which exist only on the client or server (e.g. +// death ragdolls on client, logicals on server). +// Entities with associated edicts, which can cross the client/server divide. +// +// If the game tries to assign a 2049th edict it will exit with an error +// message, but if it tries to create a 2049th non-networked entity it will +// merely refuse and print a warning to the console. The logic behind this +// may be that an entity spawned dynamically (i.e. not present in the map) +// but not assigned an edict probably isn't too important. +const e_limit = 2048 + // Dict corresponds to the edict_t in Valve's documentation for the Source // engine. See here: https://developer.valvesoftware.com/wiki/Edict_t // @@ -20,16 +36,24 @@ import ( // be used on the client. type Dict struct { *Namespace - entities map[int]Entity + entities []Entity br *bit.BufReader + + // a reference to our string table of entity baseline data. For whatever + // reason, the first set of baselines sometimes come in before the classes + // are defined. + base *stbl.Table } -func NewDict() *Dict { - return &Dict{ +func NewDict(sd *stbl.Dict) *Dict { + d := &Dict{ Namespace: new(Namespace), - entities: make(map[int]Entity), + entities: make([]Entity, e_limit), br: new(bit.BufReader), + base: sd.TableForName("instancebaseline"), } + sd.WatchTable("instancebaseline", d.updateBaselines) + return d } // creates an entity with the provided id. the entity's contents data are read @@ -52,18 +76,36 @@ func (d *Dict) createEntity(id int) error { return nil } +func (d *Dict) getEntity(id int) *Entity { + if id < 0 || id >= e_limit { + Debug.Printf("edict refused getEntity request for invalid id %d", id) + return nil + } + return &d.entities[id] +} + func (d *Dict) updateEntity(id int) error { Debug.Printf("update entity id: %d\n", id) + e := d.getEntity(id) + if e == nil { + return fmt.Errorf("update entity %d refused: no such entity", id) + } + e.Read(d.br) return nil } func (d *Dict) deleteEntity(id int) error { Debug.Printf("delete entity id: %d\n", id) + if id < 0 || id >= e_limit { + return fmt.Errorf("delete entity %d refused: no such entity", id) + } + d.entities[id] = Entity{} return nil } func (d *Dict) leaveEntity(id int) error { Debug.Printf("leave entity id: %d\n", id) + // what the shit does this do? return nil } @@ -71,9 +113,11 @@ func (d *Dict) Handle(m proto.Message) { switch v := m.(type) { case *dota.CDemoSendTables: d.mergeSendTables(v) + d.syncBaselines() case *dota.CDemoClassInfo: d.mergeClassInfo(v) + d.syncBaselines() case *dota.CSVCMsg_PacketEntities: d.mergeEntities(v) @@ -113,3 +157,38 @@ func (d *Dict) mergeEntities(m *dota.CSVCMsg_PacketEntities) error { } return nil } + +func (d *Dict) updateBaselines(t *stbl.Table) { + d.base = t + d.syncBaselines() +} + +func (d *Dict) syncBaselines() { + Debug.Printf("syncBaselines start") + if d.base == nil { + Debug.Printf("syncBaselines failed: reference to baseline string table is nil") + return + } + + for _, e := range d.base.Entries() { + id, err := strconv.Atoi(e.Key) + if err != nil { + Debug.Printf("syncBaselines skipping entry with key %s: key failed to parse to integer: %v", e.Key, err) + continue + } + + c := d.ClassByNetId(id) + if c == nil { + Debug.Printf("syncBaselines skipping entry with key %s: no such class", e.Key) + continue + } + + if c.baseline == nil { + c.baseline = c.New() + } + + d.br.SetSource(e.Value) + Debug.Printf("syncBaselines has new baseline for class %v", c) + c.baseline.Read(d.br) + } +} diff --git a/ent/entity.go b/ent/entity.go index f67542e..9fe9845 100644 --- a/ent/entity.go +++ b/ent/entity.go @@ -10,6 +10,10 @@ type Entity struct { *Class } -func (e *Entity) Read(br bit.Reader) { - fmt.Printf("Entity %v read\n", e) +func (e *Entity) Read(br bit.Reader) error { + if e.Class == nil { + return fmt.Errorf("unable to read entity: entity has no class") + } + Debug.Printf("Entity %v read", e) + return nil } diff --git a/ent/namespace.go b/ent/namespace.go index 13a6552..ad77ea7 100644 --- a/ent/namespace.go +++ b/ent/namespace.go @@ -39,8 +39,9 @@ func (n *Namespace) mergeClassInfo(ci *dota.CDemoClassInfo) { // merges the send table data found in the replay protobufs. The send table // data contains a specification for an entity type system. -func (n *Namespace) mergeSendTables(st *dota.CDemoSendTables) { +func (n *Namespace) mergeSendTables(st *dota.CDemoSendTables) error { // sendtables only has one field, a binary data field. + Debug.Printf("merge send tables") data := st.GetData() br := bit.NewBytesReader(data) @@ -52,8 +53,7 @@ func (n *Namespace) mergeSendTables(st *dota.CDemoSendTables) { flat := dota.CSVCMsg_FlattenedSerializer{} if err := proto.Unmarshal(buf, &flat); err != nil { - fmt.Printf("error: %v\n", err) - return + return fmt.Errorf("unable to merge send tables: %v", err) } n.SymbolTable = SymbolTable(flat.GetSymbols()) @@ -67,12 +67,14 @@ func (n *Namespace) mergeSendTables(st *dota.CDemoSendTables) { n.classesByName = make(map[string]map[int]*Class, len(flat.GetSerializers())) for _, c := range flat.GetSerializers() { - class := Class{} - class.fromProto(c, fields) - name := n.Symbol(int(c.GetSerializerNameSym())) - class.Name = name version := int(c.GetSerializerVersion()) + + class := Class{Name: name, Version: version} + class.fromProto(c, fields) + + Debug.Printf("new class: %v", class) + id := classId{name: name, version: version} n.classes[id] = &class @@ -82,6 +84,8 @@ func (n *Namespace) mergeSendTables(st *dota.CDemoSendTables) { n.classesByName[name.String()][version] = &class } } + + return br.Err() } func (n *Namespace) readClassId(r bit.Reader) int { @@ -91,3 +95,22 @@ func (n *Namespace) readClassId(r bit.Reader) int { func (n *Namespace) Class(name string, version int) *Class { return n.classesByName[name][version] } + +func (n *Namespace) ClassByNetId(id int) *Class { + name, ok := n.classIds[id] + if !ok { + Debug.Printf("can't find class name for net id %d", id) + return nil + } + versions, newest := n.classesByName[name], -1 + for v, _ := range versions { + if v > newest { + newest = v + } + } + if newest == -1 { + Debug.Printf("class %s has no known versions in its version map", name) + return nil + } + return versions[newest] +} diff --git a/main.go b/main.go index 95279e9..5b32148 100644 --- a/main.go +++ b/main.go @@ -142,8 +142,12 @@ func main() { handle = dumpClasses case "entities": ent.Debug = log.New(os.Stdout, "", 0) - d := ent.NewDict() - handle = d.Handle + sd := stbl.NewDict() + ed := ent.NewDict(sd) + handle = func(m proto.Message) { + sd.Handle(m) + ed.Handle(m) + } default: bail(1, "no such action: %s", flag.Arg(0)) } diff --git a/stbl/dict.go b/stbl/dict.go index 67648e9..604f4a6 100644 --- a/stbl/dict.go +++ b/stbl/dict.go @@ -13,18 +13,20 @@ import ( // Dict represents a dictionary of string tables. Each table may be referenced // by either a numeric ID or its name. type Dict struct { - byId []Table - byName map[string]*Table - br *bit.BufReader - scratch []byte + byId []Table + byName map[string]*Table + br *bit.BufReader + scratch []byte + observers map[string][]func(*Table) } func NewDict() *Dict { return &Dict{ - byId: make([]Table, 0, 64), - byName: make(map[string]*Table, 64), - br: new(bit.BufReader), - scratch: make([]byte, 1<<16), + byId: make([]Table, 0, 64), + byName: make(map[string]*Table, 64), + br: new(bit.BufReader), + scratch: make([]byte, 1<<16), + observers: make(map[string][]func(*Table)), } } @@ -41,6 +43,7 @@ func (d *Dict) newTable(name string) *Table { // retained in the dict, but a pointer to the table is also returned in case // the newly-created table is of use to the caller. func (d *Dict) Create(m *dota.CSVCMsg_CreateStringTable) (*Table, error) { + defer d.notifyObservers(m.GetName()) Debug.Printf("create table %s", m.GetName()) t := d.newTable(m.GetName()) @@ -83,6 +86,7 @@ func (d *Dict) Update(m *dota.CSVCMsg_UpdateStringTable) error { if t == nil { return fmt.Errorf("no known string table for id %d", m.GetTableId()) } + defer d.notifyObservers(t.name) d.br.SetSource(m.GetStringData()) return t.updateEntries(d.br, int(m.GetNumChangedEntries())) @@ -112,3 +116,16 @@ func (d *Dict) Handle(m proto.Message) { Debug.Println("ignoring a full stringtable dump") } } + +func (d *Dict) WatchTable(name string, fn func(*Table)) { + if d.observers[name] == nil { + d.observers[name] = make([]func(*Table), 0, 8) + } + d.observers[name] = append(d.observers[name], fn) +} + +func (d *Dict) notifyObservers(name string) { + for _, fn := range d.observers[name] { + fn(d.TableForName(name)) + } +} diff --git a/stbl/entry.go b/stbl/entry.go index fbcb8a6..dc13133 100644 --- a/stbl/entry.go +++ b/stbl/entry.go @@ -8,21 +8,21 @@ import ( // Entry represents a single record in a string table. It's not called "Record" // because it's called "Entry" in the protobufs. type Entry struct { - key string - value []byte + Key string + Value []byte } func (e Entry) String() string { - if e.value == nil { - return fmt.Sprintf("{%s nil}", e.key) + if e.Value == nil { + return fmt.Sprintf("{%s nil}", e.Key) } - if utf8.Valid(e.value) { - return fmt.Sprintf("{%s %s}", e.key, e.value) + if utf8.Valid(e.Value) { + return fmt.Sprintf("{%s %s}", e.Key, e.Value) } - if len(e.value) > 32 { - return fmt.Sprintf("{%s 0x%x}", e.key, e.value[:32]) + if len(e.Value) > 32 { + return fmt.Sprintf("{%s 0x%x}", e.Key, e.Value[:32]) } - return fmt.Sprintf("{%s 0x%x}", e.key, e.value) + return fmt.Sprintf("{%s 0x%x}", e.Key, e.Value) } diff --git a/stbl/table.go b/stbl/table.go index cd38f15..7a76024 100644 --- a/stbl/table.go +++ b/stbl/table.go @@ -10,9 +10,13 @@ type Table struct { name string entries []Entry byteSize int - bitSize int // this is in the protobuf message but I don't know what it does. + + // this is in the protobuf message but I don't know what it does. + bitSize int } +func (t *Table) Entries() []Entry { return t.entries } + // creates n entries from the bit stream br func (t *Table) createEntries(br *bit.BufReader, n int) error { Debug.Printf("table %s create %d entries", t.name, n) @@ -38,22 +42,22 @@ func (t *Table) createEntries(br *bit.BufReader, n int) error { // backreading flag: indicates that the key references an earlier // key or a portion of an earlier key as a prefix if bit.ReadBool(br) { - entry.key = t.entries[base+br.ReadBits(5)].key[:br.ReadBits(5)] + bit.ReadString(br) + entry.Key = t.entries[base+br.ReadBits(5)].Key[:br.ReadBits(5)] + bit.ReadString(br) } else { - entry.key = bit.ReadString(br) + entry.Key = bit.ReadString(br) } } // value flag: indicates that a value is present if bit.ReadBool(br) { if t.byteSize != 0 { - entry.value = make([]byte, t.byteSize) - br.Read(entry.value) + entry.Value = make([]byte, t.byteSize) + br.Read(entry.Value) } else { size := br.ReadBits(14) br.ReadBits(3) // ??? - entry.value = make([]byte, size) - br.Read(entry.value) + entry.Value = make([]byte, size) + br.Read(entry.Value) } } } @@ -91,32 +95,32 @@ func (t *Table) updateEntries(br *bit.BufReader, n int) error { prev, pLen := h.at(int(br.ReadBits(5))), int(br.ReadBits(5)) if prev < len(t.entries) { prevEntry := &t.entries[prev] - entry.key = prevEntry.key[:pLen] + bit.ReadString(br) + entry.Key = prevEntry.Key[:pLen] + bit.ReadString(br) } else { return fmt.Errorf("backread error") } } else { - entry.key = bit.ReadString(br) + entry.Key = bit.ReadString(br) } } // value flag if bit.ReadBool(br) { if t.byteSize != 0 { - if entry.value == nil { - entry.value = make([]byte, t.byteSize) + if entry.Value == nil { + entry.Value = make([]byte, t.byteSize) } } else { size, _ := int(br.ReadBits(14)), br.ReadBits(3) - if len(entry.value) < size { - entry.value = make([]byte, size) + if len(entry.Value) < size { + entry.Value = make([]byte, size) } else { - entry.value = entry.value[:size] + entry.Value = entry.Value[:size] } } - br.Read(entry.value) + br.Read(entry.Value) } - Debug.Printf("%s:%s = %x", t.name, entry.key, entry.value) + Debug.Printf("%s:%s = %x", t.name, entry.Key, entry.Value) } return nil }