diff --git a/art/dota/dota.go b/art/dota/dota.go new file mode 100644 index 0000000..69b3198 --- /dev/null +++ b/art/dota/dota.go @@ -0,0 +1,66 @@ +// Copyright 2017 Kaur Kuut +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dota + +import ( + "io/ioutil" + + "github.com/xStrom/patriot/art" +) + +const w, h = 50, 50 +const x0, y0 = 405, 280 +const x1, y1 = x0 + w - 1, y0 + h - 1 + +var resource = &art.Image{} + +func init() { + data, err := ioutil.ReadFile("data/dota.png") + if err != nil { + panic("Failed to read dota.png") + } + err = resource.ParseKeyframe(1, data, true) + if err != nil { + panic("Failed to parse dota.png") + } +} + +// Fixes any broken pixels in the provided image +func GetWork(image *art.Image, ignorePixels map[int]bool) *art.Pixel { + for x := x0; x <= x1; x++ { + for y := y0; y <= y1; y++ { + if ignorePixels[x|(y<<16)] { + continue + } + c1 := resource.At(x-x0, y-y0) + if c1 == art.Transparent { + continue + } + c2 := image.At(x, y) + if c1 != c2 { + return &art.Pixel{x, y, c1} + } + } + } + return nil +} + +// TODO: Bounds check function, so that not every art needs to be looped through after every pixel update --- make a dirty region system +func CheckPixel(x, y, c int) { + // Make sure the pixel is even in bounds + if x >= x0 && x <= x1 && y >= y0 && y <= y1 { + + } +} diff --git a/data/dota.png b/data/dota.png new file mode 100644 index 0000000..c6ae7b1 Binary files /dev/null and b/data/dota.png differ diff --git a/painter/painter.go b/painter/painter.go index c4d6ae1..e696496 100644 --- a/painter/painter.go +++ b/painter/painter.go @@ -15,10 +15,12 @@ package painter import ( + "net/http" "sync" "time" "github.com/xStrom/patriot/art" + "github.com/xStrom/patriot/art/dota" "github.com/xStrom/patriot/art/estcows" "github.com/xStrom/patriot/art/estville" "github.com/xStrom/patriot/log" @@ -45,10 +47,22 @@ func Work(wg *sync.WaitGroup, image *art.Image) { } shutdown.ShutdownLock.RUnlock() + // Sleep until we can perform the next move + sleepUntilNextMove() + inFlightLock.Lock() + var p *art.Pixel + + // #0 priority Dota 2 logo [Near mario] + if p == nil { + p = dota.GetWork(image, inFlight) + } + // #1 priority Estville [Bottom right project] - p := estville.GetWork(image, inFlight) + if p == nil { + p = estville.GetWork(image, inFlight) + } // #2 priority Estonian flag [Classic above the fold flag] /* @@ -64,125 +78,91 @@ func Work(wg *sync.WaitGroup, image *art.Image) { if p != nil { inFlight[p.X|(p.Y<<16)] = true - dc := allocateDrawCall(image.At(p.X, p.Y)) - go func(p *art.Pixel, dc *drawCall) { - if err := sp.DrawPixel(p.X, p.Y, p.C); err != nil { - dc.Cancel() + cost := drawCallCost(image.At(p.X, p.Y)) + cs := addCycleCost(cost) + go func(p *art.Pixel, cs int64, cost int) { + //log.Infof("Requesting draw of %v:%v - %v", p.X, p.Y, p.C) + if err, statusCode := sp.DrawPixel(p.X, p.Y, p.C); err != nil { + // Don't remove the cycle cost in case of 403, because that means we hit the server rate limiting + if statusCode != http.StatusForbidden { + removeCycleCost(cs, cost) + } log.Infof("Failed drawing %v:%v to %v, because: %v", p.X, p.Y, p.C, err) - } else { - dc.Finalize() } time.Sleep(5 * time.Second) // Allow another additional 5 seconds for realtime to update after the request is done inFlightLock.Lock() delete(inFlight, p.X|(p.Y<<16)) inFlightLock.Unlock() - }(p, dc) + }(p, cs, cost) } inFlightLock.Unlock() - // Sleep until we can perform the next move - if sleep := getTimeUntilNextMove(); sleep > 0 { - log.Infof("Sleeping %v", sleep) - time.Sleep(sleep) - } - } -} - -type drawCall struct { - time time.Time - cost int -} - -func (dc *drawCall) Cancel() { - drawcallsLock.Lock() - defer drawcallsLock.Unlock() - for i := range drawcalls { - if drawcalls[i] == dc { - drawcalls[i], drawcalls = drawcalls[len(drawcalls)-1], drawcalls[:len(drawcalls)-1] - return + // Prevent hot spin if there's nothing to do + if p == nil { + time.Sleep(100 * time.Millisecond) } } } -func (dc *drawCall) Finalize() { - drawcallsLock.Lock() - dc.time = time.Now() - drawcallsLock.Unlock() -} - -var drawcalls []*drawCall -var drawcallsLock sync.Mutex - const ( scorePerWindow = 50 - scoreWindow = 10 * time.Second + scoreWindowSecs = 10 paintOverWhiteCost = 2 paintOverOtherCost = 5 ) func drawCallCost(oldColor int) int { - if oldColor == art.White { - return paintOverWhiteCost - } + /* + // DISABLED: White surface reduced cost seems broken on the server side + if oldColor == art.White { + return paintOverWhiteCost + } + */ return paintOverOtherCost } -func allocateDrawCall(oldColor int) *drawCall { - dc := &drawCall{cost: drawCallCost(oldColor)} - drawcallsLock.Lock() - drawcalls = append(drawcalls, dc) - drawcallsLock.Unlock() - return dc -} +var cycleCost int +var cycleStart int64 +var cycleLock sync.Mutex -func getTimeUntilNextMove() time.Duration { - drawcallsLock.Lock() +func addCycleCost(cost int) int64 { + cycleLock.Lock() + defer cycleLock.Unlock() + cycleCost += cost + return cycleStart +} - // Clean up entries older than the configured time window - now := time.Now() - for i := 0; i < len(drawcalls); i++ { - if !drawcalls[i].time.IsZero() && now.Sub(drawcalls[i].time) >= scoreWindow { - drawcalls[i], drawcalls = drawcalls[len(drawcalls)-1], drawcalls[:len(drawcalls)-1] - i-- - } +func removeCycleCost(start int64, cost int) { + cycleLock.Lock() + defer cycleLock.Unlock() + if cycleStart == start { + cycleCost -= cost } +} - // Add up the score - score := 0 - for i := range drawcalls { - score += drawcalls[i].cost - } +func sleepUntilNextMove() { + for { + cycleLock.Lock() + + now := time.Now().Unix() + if cycleStart+scoreWindowSecs <= now { + cycleStart = now + cycleCost = 0 + //log.Infof("New cycle started at %v", cycleStart) + cycleLock.Unlock() + return + } - drawcallsLock.Unlock() + // Can we do the next move? + if scorePerWindow-cycleCost >= paintOverOtherCost { + //log.Infof("Can still do another move (%v/%v)", cycleCost, scorePerWindow) + cycleLock.Unlock() + return + } - // Can we do the next move immediately? - if scorePerWindow-score >= paintOverOtherCost { - return 0 - } + cycleLock.Unlock() - // Determine time until enough free moves for any color surface - for { - var until time.Duration - freeMoves := scorePerWindow - score - drawcallsLock.Lock() - now := time.Now() - for i := range drawcalls { - if !drawcalls[i].time.IsZero() { - freeMoves += drawcalls[i].cost - remaining := drawcalls[i].time.Sub(now) - if remaining > until { - until = remaining - } - if freeMoves >= paintOverOtherCost { - drawcallsLock.Unlock() - return until - } else { - log.Infof("Not enough free moves: %v", freeMoves) - } - } - } - drawcallsLock.Unlock() - time.Sleep(100 * time.Millisecond) + time.Sleep(1 * time.Second) } } diff --git a/sp/sp.go b/sp/sp.go index a4301f9..8ad7d10 100644 --- a/sp/sp.go +++ b/sp/sp.go @@ -85,25 +85,25 @@ func FetchImage() ([]byte, int, error) { } } -func DrawPixel(x, y, c int) error { +func DrawPixel(x, y, c int) (error, int) { t := time.Now() req, err := http.NewRequest("POST", fmt.Sprintf("https://josephg.com/sp/edit?x=%v&y=%v&c=%v", x, y, c), nil) if err != nil { - return errors.Wrap(err, "Failed creating request") + return errors.Wrap(err, "Failed creating request"), -1 } req.Header.Set("User-Agent", UserAgent) resp, err := client.Do(req) if err != nil { - return errors.Wrap(err, "Failed performing request") + return errors.Wrap(err, "Failed performing request"), -1 } if resp.StatusCode != http.StatusOK { - return errors.Errorf("Got non-OK status: %v", resp.StatusCode) + return errors.Errorf("Got non-OK status: %v", resp.StatusCode), resp.StatusCode } if b, err := ioutil.ReadAll(resp.Body); err != nil { - return errors.Wrap(err, "Failed reading response") + return errors.Wrap(err, "Failed reading response"), resp.StatusCode } else if len(b) > 0 { log.Infof("Got response:\n%v", string(b)) } log.Infof("Drew: %v - %v - %v [%dms]", x, y, c, time.Since(t)/time.Millisecond) - return nil + return nil, resp.StatusCode }