Skip to content

Commit 84396bc

Browse files
committed
feat: generate the entire dota api
1 parent d619532 commit 84396bc

38 files changed

+22820
-276
lines changed

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ Currently, the following client features have been implemented in this library:
2020
- [x] Player profile fetching / call tracking
2121
- [x] SOCache tracking / state management
2222
- [x] Basic chat interaction
23-
- [~] Lobby tracking / state management
23+
- [x] Lobby tracking / state management
2424
- [x] Read lobby state correctly
25-
- [ ] Implement normal lobby operations
26-
- [~] Party tracking / state management
25+
- [x] Implement normal lobby operations
26+
- [x] Party tracking / state management
2727
- [x] Read party and invite state correctly
28-
- [ ] Implement normal party operations
28+
- [x] Implement normal party operations
29+
- [x] Code generation for API
30+
- [ ] Code generation for events
31+
- [ ] Less awkward comment generation / handwritten comments
2932

3033
... and others. This is the current short-term roadmap.
3134

@@ -56,6 +59,14 @@ Events for the object type are emitted on the eventCh. Be sure to call `eventCan
5659

5760
The cache object also adds interfaces to get and list the current objects in the cache.
5861

62+
## Implementation Generation
63+
64+
The base API implementation is generated by the [apigen](./apigen) code. Using heuristics, request IDs are matched to response IDs, and events and action-only requests are identified. Some manual tweaking is done in the overrides file.
65+
66+
Next, the API information is used to build a Go code-gen set of implementations around the `MakeRequest` request tracking mechanism.
67+
68+
This means that ALL of the Dota API will be available in this codebase, although only some of it is documented.
69+
5970
## go-steam Dependency
6071

6172
This library depends on `go-steam`. Currently we are using the [FACEIT fork](https://github.com/faceit/go-steam).

apigen/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
apigen

apigen/api_generate.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"go/types"
7+
"io"
8+
"sort"
9+
"strings"
10+
"unicode"
11+
12+
gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"
13+
"github.com/pkg/errors"
14+
"github.com/serenize/snaker"
15+
)
16+
17+
// GenerateAPI uses protobuf reflection to generate an API client for Dota.
18+
func GenerateAPI(ctx context.Context, clientOutput, eventsOutput io.Writer) error {
19+
senderMap := make(map[gcm.EDOTAGCMsg]MsgSender)
20+
21+
packageMap, protoMap, err := BuildProtoTypeMap()
22+
if err != nil {
23+
return err
24+
}
25+
26+
fmt.Fprintf(clientOutput, "package dota2\n\nimport (\n")
27+
28+
msgIds := getSortedMsgIDs()
29+
var requestFuncs []*generatedRequestFunc
30+
31+
clientImports := make(map[string]struct{})
32+
clientImports["context"] = struct{}{}
33+
clientImports["github.com/paralin/go-dota2/protocol/dota_shared_enums"] = struct{}{}
34+
clientImports["github.com/faceit/go-steam/steamid"] = struct{}{}
35+
clientImports["github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"] = struct{}{}
36+
37+
eventsImports := make(map[string]struct{})
38+
eventsImports["context"] = struct{}{}
39+
40+
// responseMsgs are messages that are known to be responses.
41+
responseMsgs := make(map[gcm.EDOTAGCMsg]struct{})
42+
// eventHandlers := make(map[gcm.EDOTAGCMsg]*generatedEventHandler)
43+
44+
for _, msgID := range msgIds {
45+
sender := GetMessageSender(msgID)
46+
if sender == MsgSenderNone {
47+
continue
48+
}
49+
50+
senderMap[msgID] = sender
51+
if sender == MsgSenderClient {
52+
genReqFunc, err := buildGeneratedRequestFunc(msgID, protoMap, clientImports)
53+
if err != nil {
54+
return err
55+
}
56+
57+
if genReqFunc.respMsgID != 0 {
58+
responseMsgs[genReqFunc.respMsgID] = struct{}{}
59+
}
60+
61+
requestFuncs = append(requestFuncs, genReqFunc)
62+
} /* else if sender == MsgSenderGC {
63+
msgStr := msgID.String()
64+
if strings.HasSuffix(msgStr, "Response") {
65+
continue
66+
}
67+
68+
eventHandler, err := buildGeneratedEventHandler(msgID, protoMap, eventsImports)
69+
if err != nil {
70+
return err
71+
}
72+
73+
eventHandlers[msgID] = eventHandler
74+
}*/
75+
}
76+
77+
sort.Slice(requestFuncs, func(i int, j int) bool {
78+
return requestFuncs[i].methodName < requestFuncs[j].methodName
79+
})
80+
81+
for pakPath := range clientImports {
82+
fmt.Fprintf(clientOutput, "\t\"%s\"\n", pakPath)
83+
}
84+
85+
fmt.Fprintf(clientOutput, ")\n")
86+
87+
steamIDFieldOverrides := make(map[string]string)
88+
for _, f := range requestFuncs {
89+
fmt.Fprintf(clientOutput, "\n// %s\n", f.generateComment())
90+
fmt.Fprintf(clientOutput, "func (d *Dota2) %s(\n", f.methodName)
91+
92+
respTyp := f.respType
93+
if respTyp != nil {
94+
fmt.Fprintf(clientOutput, "\tctx context.Context,\n")
95+
}
96+
97+
reqTyp := f.reqType.Type()
98+
reqTypUnderlying := reqTyp.Underlying()
99+
reqDs := reqTypUnderlying.(*types.Struct)
100+
reqFields := make(map[string]string)
101+
var reqFieldsOrdered []string
102+
103+
reqObjAsArgument := reqDs.NumFields() > 8
104+
if v, ok := msgArgAsParameterOverrides[f.reqMsgID]; ok {
105+
reqObjAsArgument = v
106+
}
107+
108+
if reqObjAsArgument {
109+
fmt.Fprintf(clientOutput, "\treq *")
110+
if err := printFieldType(clientOutput, reqTyp); err != nil {
111+
return err
112+
}
113+
fmt.Fprintf(clientOutput, ",\n")
114+
} else {
115+
for i := 0; i < reqDs.NumFields(); i++ {
116+
reqField := reqDs.Field(i)
117+
if !reqField.Exported() {
118+
continue
119+
}
120+
121+
reqFieldName := reqField.Name()
122+
if strings.HasPrefix(reqFieldName, "XXX_") {
123+
continue
124+
}
125+
126+
reqFieldNameUpper := reqFieldName
127+
reqFieldName = snaker.SnakeToCamel(reqFieldName)
128+
reqFieldName = string(append([]rune{unicode.ToLower([]rune(reqFieldName)[0])}, []rune(reqFieldName[1:])...))
129+
switch reqFieldName {
130+
case "map":
131+
reqFieldName = "gameMap"
132+
case "clientVersion":
133+
continue
134+
}
135+
136+
reqFieldName = strings.Replace(reqFieldName, "Id", "ID", -1)
137+
138+
_, isSlice := reqField.Type().(*types.Slice)
139+
if isSlice {
140+
if _, ok := reqFields[reqFieldName]; !ok {
141+
reqFieldsOrdered = append(reqFieldsOrdered, reqFieldName)
142+
}
143+
reqFields[reqFieldName] = reqFieldNameUpper
144+
} else {
145+
if _, ok := reqFields["&"+reqFieldName]; !ok {
146+
reqFieldsOrdered = append(reqFieldsOrdered, "&"+reqFieldName)
147+
}
148+
reqFields["&"+reqFieldName] = reqFieldNameUpper
149+
}
150+
151+
reqFieldType := reqField.Type()
152+
reqFieldTypePtr, isPtr := reqFieldType.(*types.Pointer)
153+
if isPtr {
154+
reqFieldType = reqFieldTypePtr.Elem()
155+
}
156+
157+
if !isSlice &&
158+
strings.Contains(strings.ToLower(reqFieldName), "steamid") &&
159+
strings.Contains(reqFieldType.String(), "uint64") {
160+
reqFieldType = types.NewNamed(
161+
types.NewTypeName(
162+
0,
163+
packageMap["github.com/faceit/go-steam/steamid"],
164+
"SteamId",
165+
reqFieldType,
166+
),
167+
reqFieldType,
168+
nil,
169+
)
170+
reqFieldNameAfter := reqFieldName + "U64"
171+
if _, ok := reqFields[reqFieldNameAfter]; !ok {
172+
reqFieldsOrdered = append(reqFieldsOrdered, reqFieldNameAfter)
173+
}
174+
steamIDFieldOverrides[reqFieldNameAfter] = reqFieldName
175+
delete(reqFields, "&"+reqFieldName)
176+
reqFields[reqFieldNameAfter] = reqFieldNameUpper
177+
}
178+
179+
fmt.Fprintf(clientOutput, "\t%s ", reqFieldName)
180+
if err := printFieldType(clientOutput, reqFieldType); err != nil {
181+
return err
182+
}
183+
fmt.Fprintf(clientOutput, ",\n")
184+
}
185+
}
186+
187+
fmt.Fprintf(clientOutput, ") ")
188+
189+
if respTyp != nil {
190+
fmt.Fprintf(clientOutput, "(*")
191+
if err := printFieldType(clientOutput, respTyp.Type()); err != nil {
192+
return err
193+
}
194+
fmt.Fprintf(clientOutput, ", error) ")
195+
}
196+
fmt.Fprintf(clientOutput, "{\n")
197+
198+
// transform steam IDs
199+
for _, fieldName := range reqFieldsOrdered {
200+
if _, ok := reqFields[fieldName]; !ok {
201+
continue
202+
}
203+
204+
if sidOverride, ok := steamIDFieldOverrides[fieldName]; ok {
205+
fmt.Fprintf(clientOutput, "\t%sVal := uint64(%s)\n", fieldName, sidOverride)
206+
fmt.Fprintf(clientOutput, "\t%s := &%sVal\n", fieldName, fieldName)
207+
}
208+
}
209+
210+
if !reqObjAsArgument {
211+
fmt.Fprintf(clientOutput, "\treq := &")
212+
if err := printFieldType(clientOutput, reqTyp); err != nil {
213+
return err
214+
}
215+
fmt.Fprintf(clientOutput, "{")
216+
for _, fieldName := range reqFieldsOrdered {
217+
fieldNameUpper, ok := reqFields[fieldName]
218+
if !ok {
219+
continue
220+
}
221+
222+
fmt.Fprintf(clientOutput, "\n\t\t%s: %s,", fieldNameUpper, fieldName)
223+
}
224+
if len(reqFields) != 0 {
225+
fmt.Fprintf(clientOutput, "\n\t")
226+
}
227+
fmt.Fprintf(clientOutput, "}\n")
228+
}
229+
230+
if respTyp != nil {
231+
fmt.Fprintf(clientOutput, "\tresp := &")
232+
if err := printFieldType(clientOutput, respTyp.Type()); err != nil {
233+
return err
234+
}
235+
fmt.Fprintf(clientOutput, "{}\n\n\treturn resp, d.MakeRequest(\n")
236+
fmt.Fprintf(clientOutput, "\t\tctx,\n\t\tuint32(dota_gcmessages_msgid.EDOTAGCMsg_%s),\n", f.reqMsgID.String())
237+
fmt.Fprintf(clientOutput, "\t\treq,\n\t\tuint32(dota_gcmessages_msgid.EDOTAGCMsg_%s),\n", f.respMsgID.String())
238+
fmt.Fprintf(clientOutput, "\t\tresp,\n\t)\n")
239+
} else {
240+
fmt.Fprintf(clientOutput, "\td.write(uint32(dota_gcmessages_msgid.EDOTAGCMsg_%s), req)\n", f.reqMsgID.String())
241+
}
242+
243+
fmt.Fprintf(clientOutput, "}\n")
244+
}
245+
246+
return nil
247+
}
248+
249+
func printFieldType(output io.Writer, reqFieldType types.Type) error {
250+
switch rft := reqFieldType.(type) {
251+
case *types.Basic:
252+
fmt.Fprintf(output, "%s", rft.Name())
253+
case *types.Pointer:
254+
fmt.Fprintf(output, "*")
255+
return printFieldType(output, rft.Elem())
256+
case *types.Named:
257+
pkgName := rft.Obj().Pkg().Name()
258+
typName := rft.Obj().Name()
259+
fmt.Fprintf(output, "%s.%s", pkgName, typName)
260+
case *types.Slice:
261+
elemType := rft.Elem()
262+
fmt.Fprintf(output, "[]")
263+
return printFieldType(output, elemType)
264+
default:
265+
return errors.Errorf("type unhandled: %#v", reqFieldType)
266+
}
267+
268+
return nil
269+
}

apigen/api_generate_event.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"
5+
)
6+
7+
type generatedEventHandler struct {
8+
msgID gcm.EDOTAGCMsg
9+
eventName string
10+
eventType *ProtoType
11+
}
12+
13+
// buildGeneratedEventHandler builds a generated event handler.
14+
func buildGeneratedEventHandler(
15+
msgID gcm.EDOTAGCMsg,
16+
protoMap map[string]*ProtoType,
17+
eventImports map[string]struct{},
18+
) (*generatedEventHandler, error) {
19+
eventProtoType, err := LookupMessageProtoType(protoMap, msgID)
20+
if err != nil {
21+
return nil, err
22+
}
23+
eventImports[eventProtoType.Pak.Path()] = struct{}{}
24+
25+
return &generatedEventHandler{
26+
msgID: msgID,
27+
eventName: GetMessageEventName(msgID),
28+
eventType: eventProtoType,
29+
}, nil
30+
}

0 commit comments

Comments
 (0)