Starcraft II Demo Fetcher

This is the raw code that I did in like what… 20 minutes (okay, maybe a bit more for the v1)

version 1.0

					
            package main

            import (
            "fmt"
            "html"
            "image/color"
            "io/fs"
            "log"
            "os"
            "path/filepath"
            "regexp"
            "slices"
            "strings"
            "time"

            "github.com/icza/s2prot/rep"
            "gonum.org/v1/plot"
            "gonum.org/v1/plot/plotter"
            "gonum.org/v1/plot/vg"
            )

            func main() {
            var repPath, choice1 string
            fmt.Println("Enter the replay path: ")
            fmt.Scan(&repPath)
            fmt.Println("Do you want to auto-rename ALL of the replays (WARNING : this will create a new folder within the first one… without sub-folders) [y/N]?")
            fmt.Scan(&choice1)
            renameFlag := strings.EqualFold(choice1, "y")

            startTimer := time.Now()
            err := filepath.WalkDir(repPath, func(path string, d fs.DirEntry, err error) error {
            if !d.IsDir() && filepath.Ext(path) == ".SC2Replay" {
            fmt.Printf("\nProcessing: %v\n", path)
            scanReplay(path)
            if renameFlag {
            moveAndRename(path)
            }

            }
            return err
            })
            if err != nil {
            log.Fatalf("\nError: %v", err)
            }
            fmt.Printf("Scanned in : %v", time.Since(startTimer))
            }
            func scanReplay(replayPath string) {
            r, err := rep.NewFromFile(replayPath)
            if err != nil {
            fmt.Printf("\nError: %v\n", err)
            return
            }
            defer r.Close()

            var playerList []string
            for _, pl := range r.Details.Players() {
            playerList = append(playerList, pl.Name)
            }

            // APM Graph
            func() {
            var eventList = map[int]string{
            1:   "UnitBorn",
            4:   "UnitTypeChange",
            5:   "Upgrade",
            6:   "UnitInit",
            27:  "Cmd",
            28:  "SelectionDelta",
            29:  "ControlGroupUpdate",
            49:  "CameraUpdate",
            104: "CmdUpdateTargetPoint",
            105: "CmdUpdateTargetUnit",
            }
            p := plot.New()

            p.Title.Text = "APM Graph" + " -- " + r.Details.Matchup() + " - " + r.Details.Time().String() + " - " + r.Details.Title() + strings.Join(playerList, "_")

            p.X.Label.Text = "Time"
            p.X.Max = float64(r.Header.Loops())

            p.Y.Label.Text = "Actions"
            p.Y.AutoRescale = true

            var loopsPerMinute int64 = 1344 // 22.4 actions per second * 60 seconds
            for pID, player := range r.Details.Players() {
            pt := plotter.XYs{}
            var actionLoops []int64
            for _, event := range r.GameEvts {
            if eventList[event.EvtType.ID] == event.EvtType.Name && event.Loop() != 0 && event.UserID() == player.WorkingSetSlotID() {
            actionLoops = append(actionLoops, event.Loop())
            }
            }
            for _, track := range r.TrackerEvts.Evts {
            if eventList[track.EvtType.ID] == track.EvtType.Name && track.Loop() != 0 && track.UserID() == player.WorkingSetSlotID() {
            actionLoops = append(actionLoops, track.Loop())
            }
            }
            slices.Sort(actionLoops)

            if len(actionLoops) > 0 {
            numBuckets := int(actionLoops[len(actionLoops)-1]/loopsPerMinute) + 1
            counts := make([]int, numBuckets)

            for _, loop := range actionLoops {
            bucketIdx := loop / loopsPerMinute
            counts[int(bucketIdx)]++
            }
            for i, c := range counts {
            pt = append(pt, plotter.XY{
            X: float64(i) * float64(loopsPerMinute),
            Y: float64(c),
            })
            }
            }

            line, err := plotter.NewLine(pt)
            if err != nil {
            fmt.Printf("\tError: %v\n", err)
            }
            line.LineStyle.Color = setColor(pID)
            p.Add(line)
            p.Legend.Add(player.Name, line)
            }
            errSav := p.Save(12*vg.Inch, 12*vg.Inch, "APMGRAPH--"+r.Details.Title()+"-"+r.Details.Matchup()+"-"+r.Details.Time().String()+".svg")
            if errSav != nil {
            fmt.Printf("\tError: %v\n", errSav)
            }
            }()

            // ControlGroup graph
            func() {
            p := plot.New()
            p.Title.Text = "Control Group Graph" + " -- " + r.Details.Matchup() + " - " + r.Details.Time().String() + " - " + r.Details.Title() + strings.Join(playerList, "_")
            p.X.Label.Text = "Time"
            p.X.AutoRescale = true
            p.Y.Label.Text = "Group"
            p.Y.Min = 0
            p.Y.Max = 9

            p.Y.Tick.Marker = plot.ConstantTicks([]plot.Tick{
            {Value: 0, Label: "0"},
            {Value: 1, Label: "1"},
            {Value: 2, Label: "2"},
            {Value: 3, Label: "3"},
            {Value: 4, Label: "4"},
            {Value: 5, Label: "5"},
            {Value: 6, Label: "6"},
            {Value: 7, Label: "7"},
            {Value: 8, Label: "8"},
            {Value: 9, Label: "9"},
            })

            for pID, player := range r.Details.Players() {
            pt := plotter.XYs{}

            for _, event := range r.GameEvts {
            if event.UserID() == player.WorkingSetSlotID() && event.Stringv("evtTypeName") == "ControlGroupUpdate" {
            if event.Int("controlGroupUpdate") != event.Int("controlGroupIndex") {
            pt = append(pt, plotter.XY{X: float64(event.Loop()), Y: float64(event.Int("controlGroupIndex"))})
            }
            if event.Int("controlGroupUpdate") == event.Int("controlGroupIndex") {
            pt = append(pt, plotter.XY{X: float64(event.Loop()), Y: float64(event.Int("controlGroupUpdate"))})
            }
            }
            }
            scatter, err := plotter.NewScatter(pt)
            if err != nil {
            fmt.Printf("\tError: %v\n", err)
            }
            scatter.GlyphStyle.Color = setColor(pID)
            scatter.GlyphStyle.Radius = vg.Points(3)

            p.Add(scatter)
            p.Legend.Add(player.Name, scatter)
            }
            errSav := p.Save(12*vg.Inch, 6*vg.Inch, "ControlGroupGRAPH--"+r.Details.Title()+"-"+r.Details.Matchup()+"-"+r.Details.Time().String()+".svg")
            if errSav != nil {
            fmt.Printf("\tError: %v\n", errSav)
            }

            }()
            // Build Order graph
            // func() {
            //      var unitFilter = map[string]bool{}
            //      for _, player := range r.Details.Players() {
            //              for _, track := range r.TrackerEvts.Evts {
            //                      if track.UserID() == player.WorkingSetSlotID() && track.Loop() != 0 {
            //                              switch track.EvtType.ID {
            //                              case rep.TrackerEvtIDUnitBorn:
            //                              }
            //                      }
            //              }
            //      }
            // }()
            }
            func setColor(playerID int) color.RGBA {
            switch playerID {
            case 0:
            return color.RGBA{R: 255, G: 0, B: 0, A: 255}
            case 1:
            return color.RGBA{R: 0, G: 255, B: 0, A: 255}
            case 2:
            return color.RGBA{R: 0, G: 0, B: 255, A: 255}
            case 3:
            return color.RGBA{R: 255, G: 255, B: 0, A: 255}
            case 4:
            return color.RGBA{R: 255, G: 0, B: 255, A: 255}
            case 5:
            return color.RGBA{R: 0, G: 255, B: 255, A: 255}
            case 6:
            return color.RGBA{R: 255, G: 255, B: 255, A: 255}
            case 7:
            return color.RGBA{R: 128, G: 128, B: 128, A: 255}
            }
            return color.RGBA{R: 255, G: 255, B: 255, A: 128}
            }

            func moveAndRename(replayPath string) {
            dir := filepath.Dir(replayPath)
            ext := filepath.Ext(replayPath)
            r, err := rep.NewFromFile(replayPath)
            if err != nil {
            fmt.Printf("Error: %v\n", err)
            return
            }
            var result string
            var playerList []string
            for pID, player := range r.Details.Players() {
            cleanName := html.UnescapeString(player.Name)
            cleanName = strings.NewReplacer("<", "", ">", "").Replace(cleanName)
            playerList = append(playerList, cleanName)
            if pID == 0 && player.WorkingSetSlotID() == int64(pID) {
            result = player.Result().String()
            }
            }
            newName := fmt.Sprintf(
            "%s-%s_%s_%s-%s%s",
            result,
            strings.Join(playerList, "-"),
            r.Details.Matchup(),
            r.Details.Title(),
            r.Details.Time().Format("2006-01-02 15:04:05"),
            ext,
            )
            newName = sanitizeFilename(newName)
            newPath := filepath.Join(dir, newName)
            if newPath == replayPath {
            fmt.Printf("Error: %v\n", err)
            return
            }
            os.Rename(replayPath, newPath)
            fmt.Printf("Renamed : %s -> %s\n", filepath.Base(replayPath), newName)
            }
            func sanitizeFilename(name string) string {
            var invalidChars = regexp.MustCompile(`[\\/:*?"<>|]`)
            return invalidChars.ReplaceAllString(name, "_")
            }
					
				

version 0.1b

					
						package main
					
						import (
						"fmt"
						"io/fs"
						"log"
						"path/filepath"
						"time"
					
						"github.com/icza/s2prot"
						"github.com/icza/s2prot/rep"
						)
					
						func main() {
						var repPath string
						fmt.Print("Enter the replay folder path: ")
						fmt.Scan(&repPath)
						startTime := time.Now()
						cleanRepPath := filepath.Clean(repPath)
					
						err := filepath.WalkDir(cleanRepPath, func(path string, d fs.DirEntry, err error) error {
						if !d.IsDir() && filepath.Ext(path) == ".SC2Replay" {
						scanReplay(path)
						}
						return err
						})
						if err != nil {
						log.Fatalf("Cannot enter the folder: %v\n", err)
						}
						initFetcher()
						fmt.Printf("It took %v to process", time.Since(startTime))
						}
						func scanReplay(rPath string) {
						r, err := rep.NewFromFile(rPath)
						if err != nil {
						fmt.Printf("Cannot scan the file: %v\n", err)
						return
						}
						fmt.Printf("BaseBuild     : %v\n", r.Header.BaseBuild())
						fmt.Printf("Loops         : %v\n", r.Header.Loops())
						fmt.Printf("Map Name      : %v\n", r.Details.Title())
						fmt.Printf("Map Time      : %v\n", r.Details.Time())
						fmt.Printf("Speed         : %v\n", r.Details.GameSpeed())
						fmt.Printf("Matchup       : %v\n", r.Details.Matchup())
						fmt.Printf("Length        : %v\n", r.Header.Duration())
						fmt.Printf("Game Events   : %v\n", len(r.GameEvts))
						fmt.Printf("Message Events: %v\n", len(r.MessageEvts))
						fmt.Printf("Tracker Events: %v\n", len(r.TrackerEvts.Evts))
						fmt.Println("Players       :")
						for id, player := range r.Details.Players() {
						pName := player.Name
						pRace := player.RaceString()
						pResult := player.Result()
						pHandicap := player.Handicap()
						fmt.Printf("Player: %-20s, Race: %v, Handicap: %v, APM (Metadata): %v, APM (Computed): %v, MMR: %v, Result: %s\n", pName, pRace, pHandicap, FetchMeta("APM", id, r), ComputeAPM(id, r), FetchMeta("MMR", id, r), pResult)
						fmt.Println("Build Order:")
						FetchOrder("bo", id, r)
					
						fmt.Println("Group Order:")
						FetchOrder("go", id, r)
						}
						}
					
						func FetchOrder(option string, id int, file *rep.Rep) {
						switch option {
						case "bo":
						for _, track := range file.TrackerEvts.Evts {
						var IgnoreList = map[string]bool{
						"Larva":       true,
						"Broodling":   true,
						"Interceptor": true,
						}
						if track.Int("controlPlayerId")-1 == int64(id) && track.Loop() != 0 && !IgnoreList[track.Stringv("unitTypeName")] {
						var uName string
						switch track.EvtType.ID {
						case 1:
						uName = track.Stringv("unitTypeName")
						case 6:
						uName = track.Stringv("unitTypeName")
						}
						fmt.Printf("\t\tUnit: %-20s, Loop: %v, Time: %s\n", uName, track.Loop(), FormatTime(ComputeTime(track.Loop(), file.Header.Loops(), file.Header.Duration())))
						}
						}
						case "go":
						var added int64
						for _, event := range file.GameEvts {
						if event.Int("userId") == int64(id) {
						switch event.EvtType.ID {
						case rep.GmEIdCtrlGroupUpdate:
						fmt.Printf("\t\tGroup: %v, Loop: %v, Time: %s\n", event.Int("controlGroupIndex"), event.Loop(), FormatTime(ComputeTime(event.Loop(), file.Header.Loops(), file.Header.Duration())))
						case rep.GmEIdSelDelta:
						selDelta := event.Structv("delta")
						added += func() int64 {
						var Count int64
						for _, sg := range selDelta.Array("addSubgroups") {
						subgroup := sg.(s2prot.Struct)
						Count = subgroup.Int("count")
						}
						return Count
						}()
						}
						}
						}
						fmt.Printf("\n\t\tAssignations: %v\n", added)
						}
						}
					
						func ComputeAPM(pID int, file *rep.Rep) int {
						var action float64
						var ActionList = map[int]string{
						1:   "UnitBorn",
						4:   "UnitTypeChange",
						5:   "Upgrade",
						6:   "UnitInit",
						27:  "Cmd",
						28:  "SelectionDelta",
						29:  "ControlGroupUpdate",
						49:  "CameraUpdate",
						105: "CmdUpdateTargetUnit",
						104: "CmdUpdateTargetPoint",
						}
						for _, event := range file.GameEvts {
						if event.UserID() == int64(pID) && ActionList[event.EvtType.ID] == event.EvtType.Name {
						action++
						}
						}
						for _, track := range file.TrackerEvts.Evts {
						if track.UserID() == int64(pID) && ActionList[track.EvtType.ID] == track.EvtType.Name {
						action++
						}
						}
						if action > 0 && file.Header.Duration() > 0 {
						action /= file.Header.Duration().Minutes()
						return int(action)
						}
						return -1
						}
					
						func FetchMeta(option string, pID int, file *rep.Rep) int {
						for _, md := range file.Metadata.Players() {
						if md.PlayerID()-1 == int64(pID) {
						switch option {
						case "APM":
						return int(md.APM())
						case "MMR":
						return int(md.MMR())
						}
						}
						}
						return -1
						}
					
						func ComputeTime(loop int64, totalLoops int64, totalDuration time.Duration) time.Duration {
						SecondsPerLoop := totalDuration.Seconds() / float64(totalLoops)
						elapsedSec := float64(loop) * SecondsPerLoop
						return time.Duration(elapsedSec * float64(time.Second))
						}
						func FormatTime(dur time.Duration) string {
						total := int(dur.Seconds())
						return fmt.Sprintf("%02d:%02d", total/60, total%60)
						}
					
				


Return to the lobby