|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "image" |
| 6 | + "image/color" |
| 7 | + "image/draw" |
| 8 | + "image/jpeg" |
| 9 | + _ "image/jpeg" |
| 10 | + "log" |
| 11 | + "os" |
| 12 | + |
| 13 | + r3 "github.com/golang/geo/r3" |
| 14 | + draw2dimg "github.com/llgcode/draw2d/draw2dimg" |
| 15 | + |
| 16 | + dem "github.com/markus-wa/demoinfocs-golang" |
| 17 | + common "github.com/markus-wa/demoinfocs-golang/common" |
| 18 | + events "github.com/markus-wa/demoinfocs-golang/events" |
| 19 | + ex "github.com/markus-wa/demoinfocs-golang/examples" |
| 20 | +) |
| 21 | + |
| 22 | +type nadePath struct { |
| 23 | + wep common.EquipmentElement |
| 24 | + path []r3.Vector |
| 25 | + team common.Team |
| 26 | +} |
| 27 | + |
| 28 | +var ( |
| 29 | + colorFire color.Color = color.RGBA{0xff, 0x00, 0x00, 0xff} // Red |
| 30 | + colorHE color.Color = color.RGBA{0xff, 0xff, 0x00, 0xff} // Yellow |
| 31 | + colorFlash color.Color = color.RGBA{0x00, 0x00, 0xff, 0xff} // Blue, because of the color on the nade |
| 32 | + colorSmoke color.Color = color.RGBA{0xbe, 0xbe, 0xbe, 0xff} // Light gray |
| 33 | + colorDecoy color.Color = color.RGBA{0x96, 0x4b, 0x00, 0xff} // Brown, because it's shit :) |
| 34 | +) |
| 35 | + |
| 36 | +// Run like this: go run bouncynades.go -demo /path/to/demo.dem > bouncynades.jpg |
| 37 | +func main() { |
| 38 | + f, err := os.Open(ex.DemoPathFromArgs()) |
| 39 | + checkError(err) |
| 40 | + defer f.Close() |
| 41 | + |
| 42 | + p := dem.NewParser(f) |
| 43 | + |
| 44 | + _, err = p.ParseHeader() |
| 45 | + checkError(err) |
| 46 | + |
| 47 | + nadePaths := make([]*nadePath, 0) // Paths of detonated projectiles |
| 48 | + currentNadePaths := make(map[int]*nadePath) // Currently live projectiles |
| 49 | + |
| 50 | + storeNadePath := func(entityID int, pos r3.Vector, wep common.EquipmentElement, team common.Team) { |
| 51 | + if currentNadePaths[entityID] == nil { |
| 52 | + currentNadePaths[entityID] = &nadePath{ |
| 53 | + wep: wep, |
| 54 | + team: team, |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + currentNadePaths[entityID].path = append(currentNadePaths[entityID].path, pos) |
| 59 | + } |
| 60 | + |
| 61 | + p.RegisterEventHandler(func(e events.NadeEventIf) { |
| 62 | + ne := e.Base() |
| 63 | + |
| 64 | + var team common.Team |
| 65 | + if ne.Thrower != nil { |
| 66 | + team = ne.Thrower.Team |
| 67 | + } |
| 68 | + |
| 69 | + storeNadePath(ne.NadeEntityID, ne.Position, ne.NadeType, team) |
| 70 | + nadePaths = append(nadePaths, currentNadePaths[ne.NadeEntityID]) |
| 71 | + delete(currentNadePaths, ne.NadeEntityID) |
| 72 | + }) |
| 73 | + |
| 74 | + p.RegisterEventHandler(func(e events.NadeProjectileThrownEvent) { |
| 75 | + // Save previous projectile and delete from current, just a safeguard for missing NadeEvents |
| 76 | + np := currentNadePaths[e.Projectile.EntityID] |
| 77 | + if np != nil { |
| 78 | + nadePaths = append(nadePaths, np) |
| 79 | + delete(currentNadePaths, e.Projectile.EntityID) |
| 80 | + } |
| 81 | + |
| 82 | + storeNadePath(e.Projectile.EntityID, e.Projectile.Position, e.Projectile.Weapon, e.Projectile.Thrower.Team) |
| 83 | + }) |
| 84 | + |
| 85 | + p.RegisterEventHandler(func(e events.NadeProjectileBouncedEvent) { |
| 86 | + storeNadePath(e.Projectile.EntityID, e.Projectile.Position, e.Projectile.Weapon, e.Projectile.Thrower.Team) |
| 87 | + }) |
| 88 | + |
| 89 | + var nadePathsFirstHalf []*nadePath |
| 90 | + round := 0 |
| 91 | + p.RegisterEventHandler(func(events.RoundEndedEvent) { |
| 92 | + round++ |
| 93 | + // Very cheap first half check (we only want the first teams CT-side nades in the example). |
| 94 | + // Won't work with demos that have match-restarts etc. |
| 95 | + if round == 15 { |
| 96 | + nadePathsFirstHalf = nadePaths |
| 97 | + nadePaths = make([]*nadePath, 0) |
| 98 | + } |
| 99 | + }) |
| 100 | + |
| 101 | + err = p.ParseToEnd() |
| 102 | + checkError(err) |
| 103 | + |
| 104 | + // Draw image |
| 105 | + |
| 106 | + // Create output canvas |
| 107 | + dest := image.NewRGBA(image.Rect(0, 0, 1024, 1024)) |
| 108 | + |
| 109 | + // Use cache map overview as base |
| 110 | + fCache, err := os.Open("../de_cache.jpg") |
| 111 | + checkError(err) |
| 112 | + |
| 113 | + imgCache, _, err := image.Decode(fCache) |
| 114 | + checkError(err) |
| 115 | + draw.Draw(dest, dest.Bounds(), imgCache, image.Point{0, 0}, draw.Src) |
| 116 | + |
| 117 | + // Initialize the graphic context |
| 118 | + gc := draw2dimg.NewGraphicContext(dest) |
| 119 | + |
| 120 | + gc.SetLineWidth(1) // 1 px lines |
| 121 | + gc.SetFillColor(color.RGBA{0, 0, 0, 0}) // No fill, alpha 0 |
| 122 | + |
| 123 | + // Add any pending paths |
| 124 | + for _, np := range currentNadePaths { |
| 125 | + nadePaths = append(nadePaths, np) |
| 126 | + } |
| 127 | + |
| 128 | + for _, np := range nadePathsFirstHalf { |
| 129 | + if np.team != common.TeamCounterTerrorists { |
| 130 | + // Only draw CT nades |
| 131 | + continue |
| 132 | + } |
| 133 | + |
| 134 | + // Set colors |
| 135 | + switch np.wep { |
| 136 | + case common.EqMolotov: |
| 137 | + fallthrough |
| 138 | + case common.EqIncendiary: |
| 139 | + gc.SetStrokeColor(colorFire) |
| 140 | + |
| 141 | + case common.EqHE: |
| 142 | + gc.SetStrokeColor(colorHE) |
| 143 | + |
| 144 | + case common.EqFlash: |
| 145 | + gc.SetStrokeColor(colorFlash) |
| 146 | + |
| 147 | + case common.EqSmoke: |
| 148 | + gc.SetStrokeColor(colorSmoke) |
| 149 | + |
| 150 | + case common.EqDecoy: |
| 151 | + gc.SetStrokeColor(colorDecoy) |
| 152 | + |
| 153 | + default: |
| 154 | + // Set alpha to 0 so we don't draw unknown stuff |
| 155 | + gc.SetStrokeColor(color.RGBA{0x00, 0x00, 0x00, 0x00}) |
| 156 | + fmt.Println("Unknown grenade type", np.wep) |
| 157 | + } |
| 158 | + |
| 159 | + // Draw path |
| 160 | + gc.MoveTo(translateX(np.path[0].X), translateY(np.path[0].Y)) // Move to a position to start the new path |
| 161 | + |
| 162 | + for _, pos := range np.path[1:] { |
| 163 | + gc.LineTo(translateX(pos.X), translateY(pos.Y)) |
| 164 | + } |
| 165 | + |
| 166 | + gc.FillStroke() |
| 167 | + } |
| 168 | + |
| 169 | + // Write to standard output |
| 170 | + jpeg.Encode(os.Stdout, dest, &jpeg.Options{ |
| 171 | + Quality: 90, |
| 172 | + }) |
| 173 | +} |
| 174 | + |
| 175 | +// Rough translations for x & y coordinates from de_cache to 1024x1024 px. |
| 176 | +// This could be done nicer by only having to provide the mapping between two source & target coordinates and the max size. |
| 177 | +// Then we could calculate the correct stretch & offset automatically. |
| 178 | + |
| 179 | +const ( |
| 180 | + stretchX = 0.18 |
| 181 | + offsetX = 414 |
| 182 | + |
| 183 | + stretchY = -0.18 |
| 184 | + offsetY = 630 |
| 185 | +) |
| 186 | + |
| 187 | +func translateX(x float64) float64 { |
| 188 | + return x*stretchX + offsetX |
| 189 | +} |
| 190 | + |
| 191 | +func translateY(y float64) float64 { |
| 192 | + return y*stretchY + offsetY |
| 193 | +} |
| 194 | + |
| 195 | +func checkError(err error) { |
| 196 | + if err != nil { |
| 197 | + log.Fatal(err) |
| 198 | + } |
| 199 | +} |
0 commit comments