package stbl import ( "fmt" "github.com/golang/protobuf/proto" "github.com/golang/snappy" "github.com/jordanorelli/hyperstone/bit" "github.com/jordanorelli/hyperstone/dota" ) // 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 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), observers: make(map[string][]func(*Table)), } } // creates a new table, appending it to our list of tables and adding an entry // in the table name index func (d *Dict) newTable(name string) *Table { d.byId = append(d.byId, Table{name: name}) t := &d.byId[len(d.byId)-1] d.byName[name] = t return t } // Creates a string table based on the provided protobuf message. The table is // 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()) if m.GetUserDataFixedSize() { t.byteSize = int(m.GetUserDataSize()) t.bitSize = int(m.GetUserDataSizeBits()) } data := m.GetStringData() if data == nil || len(data) == 0 { Debug.Printf("table %s created as empty table", m.GetName()) return t, nil } if m.GetDataCompressed() { switch string(data[:4]) { case "LZSS": return nil, fmt.Errorf("stbl: LZSS compression is not supported") default: var err error data, err = snappy.Decode(d.scratch, data) if err != nil { return nil, fmt.Errorf("stbl: decode error: %v", err) } } } d.br.SetSource(data) if err := t.createEntries(d.br, int(m.GetNumEntries())); err != nil { return nil, err } return t, nil } // updates a string table in the dict based on the data found in a protobuf // message func (d *Dict) Update(m *dota.CSVCMsg_UpdateStringTable) error { Debug.Printf("dict: update %d entries in table having id %d", m.GetNumChangedEntries(), m.GetTableId()) t := d.TableForId(int(m.GetTableId())) 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())) } func (d *Dict) TableForId(id int) *Table { if id >= len(d.byId) { Debug.Printf("bad dict access: id %d is greater than the max table id %d", id, len(d.byId)-1) return nil } return &d.byId[id] } func (d *Dict) TableForName(name string) *Table { return d.byName[name] } func (d *Dict) Handle(m proto.Message) { switch v := m.(type) { case *dota.CSVCMsg_CreateStringTable: d.Create(v) case *dota.CSVCMsg_UpdateStringTable: d.Update(v) case *dota.CSVCMsg_ClearAllStringTables: Debug.Println("clear all string tables") case *dota.CDemoStringTables: 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)) } }