/* This program generates the protobuf and SteamLanguage files from the SteamKit data. */ package main import ( "bytes" "fmt" "go/ast" "go/parser" "go/token" "io" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" ) var printCommands = false func main() { args := strings.Join(os.Args[1:], " ") found := false if strings.Contains(args, "clean") { clean() found = true } if strings.Contains(args, "steamlang") { buildSteamLanguage() found = true } if strings.Contains(args, "proto") { buildProto() found = true } if !found { os.Stderr.WriteString("Invalid target!\nAvailable targets: clean, proto, steamlang\n") os.Exit(1) } } func clean() { print("# Cleaning") cleanGlob("../protocol/**/*.pb.go") cleanGlob("../tf2/protocol/**/*.pb.go") cleanGlob("../dota/protocol/**/*.pb.go") os.Remove("../protocol/steamlang/enums.go") os.Remove("../protocol/steamlang/messages.go") } func cleanGlob(pattern string) { protos, _ := filepath.Glob(pattern) for _, proto := range protos { err := os.Remove(proto) if err != nil { panic(err) } } } func buildSteamLanguage() { print("# Building Steam Language") exePath := "./GoSteamLanguageGenerator/bin/Debug/GoSteamLanguageGenerator.exe" if runtime.GOOS != "windows" { execute("mono", exePath, "./SteamKit", "../protocol/steamlang") } else { execute(exePath, "./SteamKit", "../protocol/steamlang") } execute("gofmt", "-w", "../protocol/steamlang/enums.go", "../protocol/steamlang/messages.go") } func buildProto() { print("# Building Protobufs") buildProtoMap("steamclient", clientProtoFiles, "../protocol/protobuf") buildProtoMap("tf", tf2ProtoFiles, "../tf2/protocol/protobuf") buildProtoMap("dota", dotaProtoFiles, "../dota/protocol/protobuf") } func buildProtoMap(srcSubdir string, files map[string]string, outDir string) { os.MkdirAll(outDir, os.ModePerm) for proto, out := range files { full := filepath.Join(outDir, out) compileProto("SteamKit/Resources/Protobufs", srcSubdir, proto, full) fixProto(full) } } // Maps the proto files to their target files. // See `SteamKit/Resources/Protobufs/steamclient/generate-base.bat` for reference. var clientProtoFiles = map[string]string{ "steammessages_base.proto": "base.pb.go", "encrypted_app_ticket.proto": "app_ticket.pb.go", "steammessages_clientserver.proto": "client_server.pb.go", "steammessages_clientserver_2.proto": "client_server_2.pb.go", "content_manifest.proto": "content_manifest.pb.go", "steammessages_unified_base.steamclient.proto": "unified/base.pb.go", "steammessages_cloud.steamclient.proto": "unified/cloud.pb.go", "steammessages_credentials.steamclient.proto": "unified/credentials.pb.go", "steammessages_deviceauth.steamclient.proto": "unified/deviceauth.pb.go", "steammessages_gamenotifications.steamclient.proto": "unified/gamenotifications.pb.go", "steammessages_offline.steamclient.proto": "unified/offline.pb.go", "steammessages_parental.steamclient.proto": "unified/parental.pb.go", "steammessages_partnerapps.steamclient.proto": "unified/partnerapps.pb.go", "steammessages_player.steamclient.proto": "unified/player.pb.go", "steammessages_publishedfile.steamclient.proto": "unified/publishedfile.pb.go", } var tf2ProtoFiles = map[string]string{ "base_gcmessages.proto": "base.pb.go", "econ_gcmessages.proto": "econ.pb.go", "gcsdk_gcmessages.proto": "gcsdk.pb.go", "tf_gcmessages.proto": "tf.pb.go", "gcsystemmsgs.proto": "system.pb.go", } var dotaProtoFiles = map[string]string{ "base_gcmessages.proto": "base.pb.go", "econ_gcmessages.proto": "econ.pb.go", "gcsdk_gcmessages.proto": "gcsdk.pb.go", "dota_gcmessages_common.proto": "dota_common.pb.go", "dota_gcmessages_client.proto": "dota_client.pb.go", "dota_gcmessages_client_fantasy.proto": "dota_client_fantasy.pb.go", "gcsystemmsgs.proto": "system.pb.go", } func compileProto(srcBase, srcSubdir, proto, target string) { outDir, _ := filepath.Split(target) err := os.MkdirAll(outDir, os.ModePerm) if err != nil { panic(err) } execute("protoc", "--go_out="+outDir, "-I="+srcBase+"/"+srcSubdir, "-I="+srcBase, filepath.Join(srcBase, srcSubdir, proto)) out := strings.Replace(filepath.Join(outDir, proto), ".proto", ".pb.go", 1) err = forceRename(out, target) if err != nil { panic(err) } } func forceRename(from, to string) error { if from != to { os.Remove(to) } return os.Rename(from, to) } var pkgRegex = regexp.MustCompile(`(package \w+)`) var pkgCommentRegex = regexp.MustCompile(`(?s)(\/\*.*?\*\/\n)package`) var unusedImportCommentRegex = regexp.MustCompile("// discarding unused import .*\n") var fileDescriptorVarRegex = regexp.MustCompile(`fileDescriptor\d+`) func fixProto(path string) { // goprotobuf is really bad at dependencies, so we must fix them manually... // It tries to load each dependency of a file as a seperate package (but in a very, very wrong way). // Because we want some files in the same package, we'll remove those imports to local files. file, err := ioutil.ReadFile(path) if err != nil { panic(err) } fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, file, parser.ImportsOnly) if err != nil { panic("Error parsing " + path + ": " + err.Error()) } importsToRemove := make([]*ast.ImportSpec, 0) for _, i := range f.Imports { // We remove all local imports if i.Path.Value == "\".\"" { importsToRemove = append(importsToRemove, i) } } for _, itr := range importsToRemove { // remove the package name from all types file = bytes.Replace(file, []byte(itr.Name.Name+"."), []byte{}, -1) // and remove the import itself file = bytes.Replace(file, []byte(fmt.Sprintf("import %v %v\n", itr.Name.Name, itr.Path.Value)), []byte{}, -1) } // remove the package comment because it just includes a list of all messages and // collides not only with the other compiled protobuf files, but also our own documentation. file = cutAllSubmatch(pkgCommentRegex, file, 1) // remove warnings file = unusedImportCommentRegex.ReplaceAllLiteral(file, []byte{}) // fix the package name file = pkgRegex.ReplaceAll(file, []byte("package "+inferPackageName(path))) // fix the google dependency; // we just reuse the one from protoc-gen-go file = bytes.Replace(file, []byte("google/protobuf"), []byte("github.com/golang/protobuf/protoc-gen-go/descriptor"), -1) // we need to prefix local variables created by protoc-gen-go so that they don't clash with others in the same package filename := strings.Split(filepath.Base(path), ".")[0] file = fileDescriptorVarRegex.ReplaceAllFunc(file, func(match []byte) []byte { return []byte(filename + "_" + string(match)) }) err = ioutil.WriteFile(path, file, os.ModePerm) if err != nil { panic(err) } } func inferPackageName(path string) string { pieces := strings.Split(path, string(filepath.Separator)) return pieces[len(pieces)-2] } func cutAllSubmatch(r *regexp.Regexp, b []byte, n int) []byte { i := r.FindSubmatchIndex(b) return bytesCut(b, i[2*n], i[2*n+1]) } // Removes the given section from the byte array func bytesCut(b []byte, from, to int) []byte { buf := new(bytes.Buffer) buf.Write(b[:from]) buf.Write(b[to:]) return buf.Bytes() } func print(text string) { os.Stdout.WriteString(text + "\n") } func printerr(text string) { os.Stderr.WriteString(text + "\n") } // This writer appends a "> " after every newline so that the outpout appears quoted. type QuotedWriter struct { w io.Writer started bool } func NewQuotedWriter(w io.Writer) *QuotedWriter { return &QuotedWriter{w, false} } func (w *QuotedWriter) Write(p []byte) (n int, err error) { if !w.started { _, err = w.w.Write([]byte("> ")) if err != nil { return n, err } w.started = true } for i, c := range p { if c == '\n' { nw, err := w.w.Write(p[n : i+1]) n += nw if err != nil { return n, err } _, err = w.w.Write([]byte("> ")) if err != nil { return n, err } } } if n != len(p) { nw, err := w.w.Write(p[n:len(p)]) n += nw return n, err } return } func execute(command string, args ...string) { if printCommands { print(command + " " + strings.Join(args, " ")) } cmd := exec.Command(command, args...) cmd.Stdout = NewQuotedWriter(os.Stdout) cmd.Stderr = NewQuotedWriter(os.Stderr) err := cmd.Run() if err != nil { printerr(err.Error()) os.Exit(1) } }