Advertisement
Guest User

Siege Green Data Gatherer

a guest
Jul 15th, 2022
66
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Go 21.94 KB | None | 0 0
  1. package main
  2.  
  3. import (
  4.     "bytes"
  5.     "context"
  6.     "encoding/json"
  7.     "flag"
  8.     "fmt"
  9.     "io"
  10.     "io/ioutil"
  11.     "net/http"
  12.     "strconv"
  13.     "strings"
  14.     "sync"
  15.     "time"
  16.  
  17.     "golang.org/x/net/html"
  18.     "golang.org/x/time/rate"
  19. )
  20.  
  21. var zKillQPSFlag = flag.Float64("zkillqps", 1, "The QPS traffic to send to zKill")
  22. var zKillBurstFlag = flag.Int("zkillburst", 1, "The burst of traffic allowed to send to zKill")
  23. var outAggFile = flag.String("outstats", "agg_stats.csv", "The file to write aggregate stats csv to")
  24. var srcDataFile = flag.String("srcdata", "src_data.csv", "The file to write data source csv to")
  25.  
  26. //
  27. // ZKILL CLIENT
  28. //
  29.  
  30. const (
  31.     ccpGamesAstrahusShipTypeId = 35832
  32.     ccpGamesAthanorShipTypeId  = 35835
  33.     ccpGamesRaitaruShipTypeId  = 35825
  34.     zkillLossPages             = "https://zkillboard.com/api/losses/shipTypeID/%d/year/%d/month/%d/page/%d/"
  35.     // For some reason, the killmail details are extremely limited in JSON format. This
  36.     // means we get to scrape HTML pages instead -- joy!
  37.     zkillKillmail   = "https://zkillboard.com/kill/%d/"
  38.     zKillTimeFormat = "2006-01-02 15:04"
  39. )
  40.  
  41. // ZKillSummaries is a list of ZKillSummary.
  42. type ZKillSummaries []ZKillSummary
  43.  
  44. // ZKillSummary contains killboard identifiers and summary details of a killmail.
  45. type ZKillSummary struct {
  46.     ID     int                `json:"killmail_id"`
  47.     Detail ZKillSummaryDetail `json:"zkb"`
  48. }
  49.  
  50. // ZKillSummaryDetail has summarizable details for each killmail.
  51. type ZKillSummaryDetail struct {
  52.     LocationID     int     `json:"locationID"`
  53.     Hash           string  `json:"hash"`
  54.     FittedValue    float64 `json:"fittedValue"`
  55.     DroppedValue   float64 `json:"droppedValue"`
  56.     DestroyedValue float64 `json:"destroyedValue"`
  57.     TotalValue     float64 `json:"totalValue"`
  58.     Points         int     `json:"points"`
  59.     NPC            bool    `json:"npc"`
  60.     Solo           bool    `json:"solo"`
  61.     AWOX           bool    `json:"awox"`
  62. }
  63.  
  64. type ZKillClient struct {
  65.     client *http.Client
  66.     r      *rate.Limiter
  67. }
  68.  
  69. // get wraps HTTP GET calls and applies ZKill-specific logic to the requests.
  70. func (c *ZKillClient) get(ctx context.Context, endpoint string) (*http.Response, error) {
  71.     req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
  72.     if err != nil {
  73.         return nil, err
  74.     }
  75.     req.Header.Set("User-Agent", "Siege Green Historical Analysis")
  76.  
  77.     c.r.Wait(ctx)
  78.     return c.client.Do(req)
  79. }
  80.  
  81. // getZKillEndpoint obtains data from a ZKill API endpoint.
  82. //
  83. // dest must be a pointer.
  84. func (c *ZKillClient) getZKillEndpoint(ctx context.Context, endpoint string, dest interface{}) error {
  85.     r, err := c.get(ctx, endpoint)
  86.     if err != nil {
  87.         return err
  88.     }
  89.     defer r.Body.Close()
  90.     b, err := ioutil.ReadAll(r.Body)
  91.     if err != nil {
  92.         return err
  93.     }
  94.     return json.Unmarshal(b, dest)
  95. }
  96.  
  97. // getZKillHTML obtains the HTML page for a single killmail.
  98. func (c *ZKillClient) getZKillHTML(ctx context.Context, endpoint string) (b *bytes.Buffer, err error) {
  99.     r, err := c.get(ctx, endpoint)
  100.     if err != nil {
  101.         return
  102.     }
  103.     defer r.Body.Close()
  104.     k, err := ioutil.ReadAll(r.Body)
  105.     if err != nil {
  106.         return
  107.     }
  108.     b = bytes.NewBuffer(k)
  109.     return
  110. }
  111.  
  112. // getZKillLossPages obtains a summary page of losses for a particular type.
  113. //
  114. // See https://github.com/zKillboard/zKillboard/wiki/API-(History)
  115. func (c *ZKillClient) getZKillLossPages(ctx context.Context, typeId, year, month, page int) (d ZKillSummaries, err error) {
  116.     err = c.getZKillEndpoint(
  117.         ctx,
  118.         fmt.Sprintf(
  119.             zkillLossPages,
  120.             typeId,
  121.             year,
  122.             month,
  123.             page),
  124.         &d)
  125.     return
  126. }
  127.  
  128. // GetZKillAstrahusLossPage gets a page of summaries of Astrahus losses.
  129. func (c *ZKillClient) GetZKillAstrahusLossPage(ctx context.Context, year, month, page int) (ZKillSummaries, error) {
  130.     return c.getZKillLossPages(ctx, ccpGamesAstrahusShipTypeId, year, month, page)
  131. }
  132.  
  133. // GetZKillAthanorLossPage gets a page of summaries of Athanor losses.
  134. func (c *ZKillClient) GetZKillAthanorLossPage(ctx context.Context, year, month, page int) (ZKillSummaries, error) {
  135.     return c.getZKillLossPages(ctx, ccpGamesAthanorShipTypeId, year, month, page)
  136. }
  137.  
  138. // GetZKillRaitaruLossPage gets a page of summaries of Raitaru losses.
  139. func (c *ZKillClient) GetZKillRaitaruLossPage(ctx context.Context, year, month, page int) (ZKillSummaries, error) {
  140.     return c.getZKillLossPages(ctx, ccpGamesRaitaruShipTypeId, year, month, page)
  141. }
  142.  
  143. // GetZKillKillmail obtains data on a specific killmail.
  144. func (c *ZKillClient) GetZKillKillmail(ctx context.Context, id int) (b *bytes.Buffer, err error) {
  145.     return c.getZKillHTML(ctx, fmt.Sprintf(zkillKillmail, id))
  146. }
  147.  
  148. //
  149. // HTML PARSING
  150. //
  151.  
  152. type HTMLKillmail struct {
  153.     KillID         string
  154.     Ship           string
  155.     ShipID         string
  156.     System         string
  157.     SystemID       string
  158.     SystemSecurity string
  159.     Region         string
  160.     RegionID       string
  161.     Time           string
  162.     Attackers      []*HTMLAttacker
  163. }
  164.  
  165. func (k *HTMLKillmail) clean() {
  166.     k.KillID = strings.TrimSpace(k.KillID)
  167.     k.Ship = strings.TrimSpace(k.Ship)
  168.     k.ShipID = strings.TrimSpace(k.ShipID)
  169.     k.System = strings.TrimSpace(k.System)
  170.     k.SystemID = strings.TrimSpace(k.SystemID)
  171.     k.SystemSecurity = strings.TrimSpace(k.SystemSecurity)
  172.     k.Region = strings.TrimSpace(k.Region)
  173.     k.RegionID = strings.TrimSpace(k.RegionID)
  174.     k.Time = strings.TrimSpace(k.Time)
  175.     for _, a := range k.Attackers {
  176.         a.clean()
  177.     }
  178. }
  179.  
  180. type HTMLAttacker struct {
  181.     Pilot         string
  182.     PilotID       string
  183.     Ship          string
  184.     ShipID        string
  185.     Item          string
  186.     ItemID        string
  187.     Corporation   string
  188.     CorporationID string
  189.     Alliance      string
  190.     AllianceID    string
  191.     Damage        string
  192.     Percent       string
  193. }
  194.  
  195. func (k *HTMLAttacker) clean() {
  196.     k.Pilot = strings.TrimSpace(k.Pilot)
  197.     k.PilotID = strings.TrimSpace(k.PilotID)
  198.     k.Ship = strings.TrimSpace(k.Ship)
  199.     k.ShipID = strings.TrimSpace(k.ShipID)
  200.     k.Item = strings.TrimSpace(k.Item)
  201.     k.ItemID = strings.TrimSpace(k.ItemID)
  202.     k.Corporation = strings.TrimSpace(k.Corporation)
  203.     k.CorporationID = strings.TrimSpace(k.CorporationID)
  204.     k.Alliance = strings.TrimSpace(k.Alliance)
  205.     k.AllianceID = strings.TrimSpace(k.AllianceID)
  206.     k.Damage = strings.TrimSpace(k.Damage)
  207.     k.Percent = strings.TrimSpace(k.Percent)
  208. }
  209.  
  210. func parseZKillmailHTML(r io.Reader, d *HTMLKillmail) error {
  211.     doc, err := html.Parse(r)
  212.     if err != nil {
  213.         return err
  214.     }
  215.     err = processHTMLNode(doc, d)
  216.     if err != nil {
  217.         return err
  218.     }
  219.     d.clean()
  220.     return nil
  221. }
  222.  
  223. func processHTMLNode(n *html.Node, d *HTMLKillmail) error {
  224.     // Visit
  225.     maybeExtractKillID(n, d)
  226.     maybeExtractShip(n, d)
  227.     maybeExtractSystemAndRegion(n, d)
  228.     maybeExtractTime(n, d)
  229.     maybeExtractAttacker(n, d)
  230.  
  231.     // Recur
  232.     for c := n.FirstChild; c != nil; c = c.NextSibling {
  233.         if err := processHTMLNode(c, d); err != nil {
  234.             return err
  235.         }
  236.     }
  237.     return nil
  238. }
  239.  
  240. func maybeExtractKillID(n *html.Node, d *HTMLKillmail) {
  241.     if len(d.KillID) > 0 {
  242.         return
  243.     }
  244.     if n.Data == "link" {
  245.         hasRelCanonical := false
  246.         for _, a := range n.Attr {
  247.             hasRelCanonical = hasRelCanonical || (a.Key == "rel" && a.Val == "canonical")
  248.         }
  249.         if hasRelCanonical {
  250.             for _, a := range n.Attr {
  251.                 if a.Key == "href" {
  252.                     d.KillID = a.Val
  253.                 }
  254.             }
  255.         }
  256.     }
  257. }
  258.  
  259. func maybeExtractShip(n *html.Node, d *HTMLKillmail) {
  260.     if len(d.Ship) > 0 {
  261.         return
  262.     }
  263.     if n.Data == "Ship:" {
  264.         n = n.Parent
  265.         for n.NextSibling != nil && n.Data != "td" {
  266.             n = n.NextSibling
  267.         }
  268.         for c := n.FirstChild; c != nil; c = c.NextSibling {
  269.             if c.Data == "a" {
  270.                 n = c
  271.             }
  272.         }
  273.         if n == nil {
  274.             return
  275.         }
  276.         if n.FirstChild != nil {
  277.             d.Ship = n.FirstChild.Data
  278.         }
  279.         for _, a := range n.Attr {
  280.             if a.Key == "href" {
  281.                 d.ShipID = a.Val
  282.             }
  283.         }
  284.     }
  285. }
  286.  
  287. func maybeExtractSystemAndRegion(n *html.Node, d *HTMLKillmail) {
  288.     if len(d.System) > 0 {
  289.         return
  290.     }
  291.     if n.Data == "System:" {
  292.         n = n.Parent
  293.         for n.NextSibling != nil && n.Data != "td" {
  294.             n = n.NextSibling
  295.         }
  296.         for c := n.FirstChild; c != nil; c = c.NextSibling {
  297.             if c.Data == "a" {
  298.                 for _, a := range c.Attr {
  299.                     if a.Key == "href" && strings.Contains(a.Val, "system") {
  300.                         d.SystemID = a.Val
  301.                         if c.FirstChild != nil {
  302.                             d.System = c.FirstChild.Data
  303.                         }
  304.                     } else if a.Key == "href" && strings.Contains(a.Val, "region") {
  305.                         d.RegionID = a.Val
  306.                         if c.FirstChild != nil {
  307.                             d.Region = c.FirstChild.Data
  308.                         }
  309.                     }
  310.                 }
  311.             } else if c.Data == "span" {
  312.                 for cc := c.FirstChild; cc != nil; cc = cc.NextSibling {
  313.                     if cc.Data == "span" {
  314.                         if cc.FirstChild != nil {
  315.                             d.SystemSecurity = cc.FirstChild.Data
  316.                         }
  317.                     }
  318.                 }
  319.             }
  320.         }
  321.     }
  322. }
  323.  
  324. func maybeExtractTime(n *html.Node, d *HTMLKillmail) {
  325.     if len(d.Time) > 0 {
  326.         return
  327.     }
  328.     if n.Data == "Time:" {
  329.         n = n.Parent
  330.         for n.NextSibling != nil && n.Data != "td" {
  331.             n = n.NextSibling
  332.         }
  333.         for _, a := range n.Attr {
  334.             if a.Key == "class" && a.Val == "info_kill_dttm" {
  335.                 if n.FirstChild != nil {
  336.                     d.Time = n.FirstChild.Data
  337.                 }
  338.             }
  339.         }
  340.     }
  341. }
  342.  
  343. func maybeExtractAttacker(n *html.Node, d *HTMLKillmail) {
  344.     hasAttacker := false
  345.     if n.Data == "tr" {
  346.         for _, a := range n.Attr {
  347.             hasAttacker = hasAttacker || (a.Key == "class" && a.Val == "attacker")
  348.         }
  349.     }
  350.     if !hasAttacker {
  351.         return
  352.     }
  353.     var m HTMLAttacker
  354.     processHTMLNodeAttacker(n, &m)
  355.     d.Attackers = append(d.Attackers, &m)
  356. }
  357.  
  358. func processHTMLNodeAttacker(n *html.Node, m *HTMLAttacker) {
  359.     // Visit
  360.     maybeExtractAttackerPilot(n, m)
  361.     maybeExtractAttackerShip(n, m)
  362.     maybeExtractAttackerItem(n, m)
  363.     maybeExtractAttackerCorporation(n, m)
  364.     maybeExtractAttackerAlliance(n, m)
  365.     maybeExtractAttackerDamage(n, m)
  366.     maybeExtractAttackerPercent(n, m)
  367.  
  368.     // Recur
  369.     for c := n.FirstChild; c != nil; c = c.NextSibling {
  370.         processHTMLNodeAttacker(c, m)
  371.     }
  372. }
  373.  
  374. func maybeExtractAttackerPilot(n *html.Node, m *HTMLAttacker) {
  375.     if len(m.Pilot) > 0 {
  376.         return
  377.     }
  378.     if n.Data == "a" {
  379.         hasTooltip := false
  380.         for _, a := range n.Attr {
  381.             hasTooltip = hasTooltip || (a.Key == "rel" && a.Val == "tooltip")
  382.         }
  383.         if hasTooltip {
  384.             return
  385.         }
  386.         for _, a := range n.Attr {
  387.             if a.Key == "href" && strings.Contains(a.Val, "character") {
  388.                 if n.FirstChild != nil {
  389.                     m.Pilot = n.FirstChild.Data
  390.                 }
  391.                 m.PilotID = a.Val
  392.             }
  393.         }
  394.     }
  395. }
  396. func maybeExtractAttackerShip(n *html.Node, m *HTMLAttacker) {
  397.     if len(m.Ship) > 0 {
  398.         return
  399.     }
  400.     if n.Data == "a" {
  401.         hasTooltip := false
  402.         for _, a := range n.Attr {
  403.             hasTooltip = hasTooltip || (a.Key == "rel" && a.Val == "tooltip")
  404.         }
  405.         if hasTooltip {
  406.             return
  407.         }
  408.         for _, a := range n.Attr {
  409.             if a.Key == "href" && strings.Contains(a.Val, "ship") {
  410.                 if n.FirstChild != nil {
  411.                     m.Ship = n.FirstChild.Data
  412.                 }
  413.                 m.ShipID = a.Val
  414.             }
  415.         }
  416.     }
  417. }
  418.  
  419. func maybeExtractAttackerItem(n *html.Node, m *HTMLAttacker) {
  420.     if len(m.Item) > 0 {
  421.         return
  422.     }
  423.     if n.Data == "a" {
  424.         hasTooltip := false
  425.         for _, a := range n.Attr {
  426.             hasTooltip = hasTooltip || (a.Key == "rel" && a.Val == "tooltip")
  427.         }
  428.         if hasTooltip {
  429.             return
  430.         }
  431.         for _, a := range n.Attr {
  432.             if a.Key == "href" && strings.Contains(a.Val, "item") {
  433.                 if n.FirstChild != nil {
  434.                     m.Item = n.FirstChild.Data
  435.                 }
  436.                 m.ItemID = a.Val
  437.             }
  438.         }
  439.     }
  440. }
  441.  
  442. func maybeExtractAttackerCorporation(n *html.Node, m *HTMLAttacker) {
  443.     if len(m.Corporation) > 0 {
  444.         return
  445.     }
  446.     if n.Data == "a" {
  447.         hasTooltip := false
  448.         for _, a := range n.Attr {
  449.             hasTooltip = hasTooltip || (a.Key == "rel" && a.Val == "tooltip")
  450.         }
  451.         if hasTooltip {
  452.             return
  453.         }
  454.         for _, a := range n.Attr {
  455.             if a.Key == "href" && strings.Contains(a.Val, "corporation") {
  456.                 if n.FirstChild != nil {
  457.                     m.Corporation = n.FirstChild.Data
  458.                 }
  459.                 m.CorporationID = a.Val
  460.  
  461.             }
  462.         }
  463.     }
  464. }
  465.  
  466. func maybeExtractAttackerAlliance(n *html.Node, m *HTMLAttacker) {
  467.     if len(m.Alliance) > 0 {
  468.         return
  469.     }
  470.     if n.Data == "a" {
  471.         hasTooltip := false
  472.         for _, a := range n.Attr {
  473.             hasTooltip = hasTooltip || (a.Key == "rel" && a.Val == "tooltip")
  474.         }
  475.         if hasTooltip {
  476.             return
  477.         }
  478.         for _, a := range n.Attr {
  479.             if a.Key == "href" && strings.Contains(a.Val, "alliance") {
  480.                 if n.FirstChild != nil {
  481.                     m.Alliance = n.FirstChild.Data
  482.                 }
  483.                 m.AllianceID = a.Val
  484.             }
  485.         }
  486.     }
  487. }
  488.  
  489. func maybeExtractAttackerDamage(n *html.Node, m *HTMLAttacker) {
  490.     if len(m.Damage) > 0 {
  491.         return
  492.     }
  493.     if n.Data == "td" {
  494.         for _, a := range n.Attr {
  495.             if a.Key == "class" && strings.Contains(a.Val, "damage") {
  496.                 if n.FirstChild != nil {
  497.                     m.Damage = n.FirstChild.Data
  498.                 }
  499.             }
  500.         }
  501.     }
  502. }
  503.  
  504. func maybeExtractAttackerPercent(n *html.Node, m *HTMLAttacker) {
  505.     if len(m.Percent) > 0 {
  506.         return
  507.     }
  508.     if n.Data == "small" {
  509.         if n.FirstChild != nil {
  510.             m.Percent = n.FirstChild.Data
  511.         }
  512.     }
  513. }
  514.  
  515. //
  516. // SIGNALS ANALYSIS
  517. //
  518.  
  519. type ParsedKillmail struct {
  520.     KillID         int
  521.     Ship           string
  522.     ShipID         int
  523.     System         string
  524.     SystemID       int
  525.     SystemSecurity float64
  526.     Region         string
  527.     RegionID       string
  528.     T              time.Time
  529.     Attackers      []ParsedAttacker
  530. }
  531.  
  532. type ParsedAttacker struct {
  533.     Pilot         string
  534.     PilotID       int
  535.     Ship          string
  536.     ShipID        int
  537.     Item          string
  538.     ItemID        int
  539.     Corporation   string
  540.     CorporationID int
  541.     Alliance      string
  542.     AllianceID    int
  543.     Damage        int
  544.     Percent       float64
  545. }
  546.  
  547. func parseID(s string) (int, error) {
  548.     if len(s) == 0 {
  549.         return -1, nil
  550.     }
  551.     ss := strings.Split(s, "/")
  552.     if len(ss) != 4 {
  553.         return -1, fmt.Errorf("Unexpected ID to parse: %s", s)
  554.     }
  555.     if ss[2] == "" {
  556.         return -1, nil
  557.     }
  558.     return strconv.Atoi(ss[2])
  559. }
  560.  
  561. func parseInt(s string) (int, error) {
  562.     return strconv.Atoi(
  563.         strings.ReplaceAll(s, ",", ""))
  564. }
  565.  
  566. func parseFloat(s string) (float64, error) {
  567.     if len(s) == 0 {
  568.         return 0, nil
  569.     }
  570.     return strconv.ParseFloat(
  571.         strings.ReplaceAll(
  572.             strings.TrimSuffix(s, "%"),
  573.             ",",
  574.             ""),
  575.         64)
  576. }
  577.  
  578. func ParseKillmail(k *HTMLKillmail) (p ParsedKillmail, err error) {
  579.     p.KillID, err = parseID(strings.TrimPrefix(k.KillID, "https://zkillboard.com"))
  580.     if err != nil {
  581.         return
  582.     }
  583.     p.Ship = k.Ship
  584.     p.ShipID, err = parseID(k.ShipID)
  585.     if err != nil {
  586.         return
  587.     }
  588.     p.System = k.System
  589.     p.SystemID, err = parseID(k.SystemID)
  590.     if err != nil {
  591.         return
  592.     }
  593.     p.SystemSecurity, err = parseFloat(k.SystemSecurity)
  594.     if err != nil {
  595.         return
  596.     }
  597.     p.Region = k.Region
  598.     p.RegionID = k.RegionID
  599.     p.T, err = time.Parse(zKillTimeFormat, k.Time)
  600.     if err != nil {
  601.         return
  602.     }
  603.     for _, a := range k.Attackers {
  604.         var pa ParsedAttacker
  605.         pa, err = parseAttacker(a)
  606.         if err != nil {
  607.             return
  608.         }
  609.         p.Attackers = append(p.Attackers, pa)
  610.     }
  611.     return
  612. }
  613.  
  614. func parseAttacker(k *HTMLAttacker) (p ParsedAttacker, err error) {
  615.     p.Pilot = k.Pilot
  616.     p.PilotID, err = parseID(k.PilotID)
  617.     if err != nil {
  618.         return
  619.     }
  620.     p.Ship = k.Ship
  621.     p.ShipID, err = parseID(k.ShipID)
  622.     if err != nil {
  623.         return
  624.     }
  625.     p.Item = k.Item
  626.     p.ItemID, err = parseID(k.ItemID)
  627.     if err != nil {
  628.         return
  629.     }
  630.     p.Corporation = k.Corporation
  631.     p.CorporationID, err = parseID(k.CorporationID)
  632.     if err != nil {
  633.         return
  634.     }
  635.     p.Alliance = k.Alliance
  636.     p.AllianceID, err = parseID(k.AllianceID)
  637.     if err != nil {
  638.         return
  639.     }
  640.     p.Damage, err = parseInt(k.Damage)
  641.     if err != nil {
  642.         return
  643.     }
  644.     p.Percent, err = parseFloat(k.Percent)
  645.     if err != nil {
  646.         return
  647.     }
  648.     return
  649. }
  650.  
  651. type AggregatedSignals struct {
  652.     V  []*DateSignal
  653.     mu *sync.Mutex
  654. }
  655.  
  656. type DateSignal struct {
  657.     Day time.Time
  658.     S   *Signals
  659. }
  660.  
  661. func (ds DateSignal) String() string {
  662.     return fmt.Sprintf("%s: %s", ds.Day.Format("2006-01-02"), ds.S)
  663. }
  664.  
  665. const (
  666.     theInitiativeAllianceID = 1900696668
  667.     briscRubalPilotID       = 883889312
  668. )
  669.  
  670. type Signals struct {
  671.     TypeToNLoss     map[int]int
  672.     NKillmailsInit  int
  673.     NKillmailsBrisc int
  674.     NHighSec        int
  675.     NLowSec         int
  676.     NNullSec        int
  677.     NWormhole       int
  678.     NPochven        int
  679. }
  680.  
  681. func (s *Signals) Add(km ParsedKillmail) {
  682.     if s.TypeToNLoss == nil {
  683.         s.TypeToNLoss = make(map[int]int)
  684.     }
  685.     s.TypeToNLoss[km.ShipID]++
  686.     foundInit := false
  687.     foundBrisc := false
  688.     for _, a := range km.Attackers {
  689.         if a.AllianceID == theInitiativeAllianceID {
  690.             foundInit = true
  691.         }
  692.         if a.PilotID == briscRubalPilotID {
  693.             foundBrisc = true
  694.         }
  695.     }
  696.     if foundInit {
  697.         s.NKillmailsInit++
  698.     }
  699.     if foundBrisc {
  700.         s.NKillmailsBrisc++
  701.     }
  702.     // Colloquial "area of game" accounting
  703.     if km.SystemSecurity >= 0.49 {
  704.         s.NHighSec++
  705.     } else if km.SystemSecurity >= 0.05 {
  706.         s.NLowSec++
  707.     } else if km.Region == "Pochven" {
  708.         s.NPochven++
  709.     } else if strings.HasPrefix(km.Region, "A-R") ||
  710.         strings.HasPrefix(km.Region, "A-R0") ||
  711.         strings.HasPrefix(km.Region, "ADR0") ||
  712.         strings.HasPrefix(km.Region, "B-R0") ||
  713.         strings.HasPrefix(km.Region, "C-R0") ||
  714.         strings.HasPrefix(km.Region, "D-R0") ||
  715.         strings.HasPrefix(km.Region, "E-R0") ||
  716.         strings.HasPrefix(km.Region, "F-R0") ||
  717.         strings.HasPrefix(km.Region, "G-R0") ||
  718.         strings.HasPrefix(km.Region, "H-R0") ||
  719.         strings.HasPrefix(km.Region, "K-R0") ||
  720.         strings.HasPrefix(km.Region, "PR-01") {
  721.         s.NWormhole++
  722.     } else {
  723.         s.NNullSec++
  724.     }
  725. }
  726.  
  727. func NewAggregatedSignals() *AggregatedSignals {
  728.     return &AggregatedSignals{
  729.         mu: &sync.Mutex{},
  730.     }
  731. }
  732.  
  733. func (a *AggregatedSignals) Add(km ParsedKillmail) {
  734.     a.mu.Lock()
  735.     defer a.mu.Unlock()
  736.     day := time.Date(km.T.Year(), km.T.Month(), km.T.Day(), 0, 0, 0, 0, km.T.Location())
  737.     var found *DateSignal
  738.     for _, v := range a.V {
  739.         if v.Day == day {
  740.             found = v
  741.             break
  742.         }
  743.     }
  744.     if found == nil {
  745.         found = &DateSignal{
  746.             Day: day,
  747.             S:   &Signals{},
  748.         }
  749.         a.V = append(a.V, found)
  750.     }
  751.     found.S.Add(km)
  752. }
  753.  
  754. //
  755. // COORDINATOR
  756. //
  757.  
  758. type Coordinator struct {
  759.     pivot  time.Time
  760.     ships  []int
  761.     client *ZKillClient
  762.     ctx    context.Context
  763. }
  764.  
  765. func NewCoordinator(
  766.     pivot time.Time,
  767.     ships []int,
  768.     client *ZKillClient,
  769.     ctx context.Context) *Coordinator {
  770.     return &Coordinator{
  771.         pivot:  pivot,
  772.         ships:  ships,
  773.         client: client,
  774.         ctx:    ctx,
  775.     }
  776. }
  777.  
  778. func (c *Coordinator) Do() (as *AggregatedSignals, pkms []ParsedKillmail) {
  779.     as = NewAggregatedSignals()
  780.     end := time.Now()
  781.     durAfterPivot := end.Sub(c.pivot)
  782.     begin := c.pivot.Add(-durAfterPivot)
  783.  
  784.     fmt.Printf("Time Period Begin: %s\n", begin)
  785.     fmt.Printf("Time Period End:   %s\n", end)
  786.  
  787.     // Get all years
  788.     var years []int
  789.     if begin.Year() == end.Year() {
  790.         years = []int{end.Year()}
  791.     } else {
  792.         for y := begin.Year(); y <= end.Year(); y++ {
  793.             years = append(years, y)
  794.         }
  795.     }
  796.     // Get all months
  797.     months := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
  798.     if len(years) == 1 {
  799.         months = []int{}
  800.         for m := begin.Month(); m <= end.Month(); m++ {
  801.             months = append(months, int(m))
  802.         }
  803.     }
  804.     // TODO: >1 year boundary will not work due to month overlap
  805.  
  806.     // Fetch lists and determine bounds
  807.     nRequests := 0
  808.     var zss []ZKillSummaries
  809.     for _, y := range years {
  810.         for _, m := range months {
  811.             for _, s := range c.ships {
  812.                 for p := 1; p <= 10; p++ {
  813.                     nRequests++
  814.                     fmt.Printf("Making summary request %d for getting pages\n", nRequests)
  815.                     zs, err := c.client.getZKillLossPages(c.ctx, s, y, m, p)
  816.                     if err != nil {
  817.                         fmt.Println(err)
  818.                         continue
  819.                     }
  820.                     if len(zs) == 0 {
  821.                         break
  822.                     }
  823.                     zss = append(zss, zs)
  824.                 }
  825.             }
  826.         }
  827.     }
  828.  
  829.     // Convert list to killmails
  830.     for _, zs := range zss {
  831.         for _, zkm := range zs {
  832.             nRequests++
  833.             fmt.Printf("Making specific request %d for getting killmail %d\n", nRequests, zkm.ID)
  834.             buf, err := c.client.GetZKillKillmail(c.ctx, zkm.ID)
  835.             if err != nil {
  836.                 fmt.Println(err)
  837.                 continue
  838.             }
  839.  
  840.             var km HTMLKillmail
  841.             err = parseZKillmailHTML(buf, &km)
  842.             if err != nil {
  843.                 fmt.Println(err)
  844.                 continue
  845.             }
  846.             pkm, err := ParseKillmail(&km)
  847.             if err != nil {
  848.                 fmt.Println(err)
  849.                 continue
  850.             }
  851.             if pkm.T.Before(begin) {
  852.                 fmt.Printf("Skipping %d as %s is before %s", pkm.KillID, pkm.T, begin)
  853.                 continue
  854.             }
  855.             pkms = append(pkms, pkm)
  856.             as.Add(pkm)
  857.         }
  858.     }
  859.     return
  860. }
  861.  
  862. //
  863. // FILE WRITING
  864. //
  865.  
  866. func (a *AggregatedSignals) Write(filename string, ships []int) error {
  867.     a.mu.Lock()
  868.     defer a.mu.Unlock()
  869.     var buf bytes.Buffer
  870.  
  871.     fmt.Fprint(&buf, "date,n_hi,n_low,n_null,n_wh,n_poch,n_init,n_brisc")
  872.     for _, s := range ships {
  873.         fmt.Fprintf(&buf, ",ship_%d", s)
  874.     }
  875.     fmt.Fprintln(&buf)
  876.     for _, d := range a.V {
  877.         fmt.Fprintf(
  878.             &buf,
  879.             "%s,%d,%d,%d,%d,%d,%d,%d",
  880.             d.Day,
  881.             d.S.NHighSec,
  882.             d.S.NLowSec,
  883.             d.S.NNullSec,
  884.             d.S.NWormhole,
  885.             d.S.NPochven,
  886.             d.S.NKillmailsInit,
  887.             d.S.NKillmailsBrisc)
  888.         for _, s := range ships {
  889.             fmt.Fprintf(&buf, ",%d", d.S.TypeToNLoss[s])
  890.         }
  891.         fmt.Fprintln(&buf)
  892.     }
  893.     return ioutil.WriteFile(filename, buf.Bytes(), 0644)
  894. }
  895.  
  896. func WriteParsedKillmails(pkms []ParsedKillmail, filename string) error {
  897.     var buf bytes.Buffer
  898.  
  899.     fmt.Fprintln(&buf, "date,kill_id,ship,ship_id,system,system_id,sec,region,region_id,attackers")
  900.     for _, pkm := range pkms {
  901.         fmt.Fprintf(
  902.             &buf,
  903.             "%s,%d,%s,%d,%s,%d,%f,%s,%s,",
  904.             pkm.T,
  905.             pkm.KillID,
  906.             pkm.Ship,
  907.             pkm.ShipID,
  908.             pkm.System,
  909.             pkm.SystemID,
  910.             pkm.SystemSecurity,
  911.             pkm.Region,
  912.             pkm.RegionID)
  913.         for _, pa := range pkm.Attackers {
  914.             fmt.Fprintf(
  915.                 &buf,
  916.                 ",%s,%d,%s,%d,%s,%d,%s,%d,%s,%d,%d,%f",
  917.                 pa.Pilot,
  918.                 pa.PilotID,
  919.                 pa.Ship,
  920.                 pa.ShipID,
  921.                 pa.Item,
  922.                 pa.ItemID,
  923.                 pa.Corporation,
  924.                 pa.CorporationID,
  925.                 pa.Alliance,
  926.                 pa.AllianceID,
  927.                 pa.Damage,
  928.                 pa.Percent)
  929.         }
  930.         fmt.Fprintln(&buf)
  931.     }
  932.     return ioutil.WriteFile(filename, buf.Bytes(), 0644)
  933. }
  934.  
  935. func main() {
  936.  
  937.     zkc := &ZKillClient{
  938.         &http.Client{},
  939.         rate.NewLimiter(rate.Limit(*zKillQPSFlag), *zKillBurstFlag),
  940.     }
  941.     ships := []int{
  942.         ccpGamesAstrahusShipTypeId,
  943.         ccpGamesAthanorShipTypeId,
  944.         ccpGamesRaitaruShipTypeId,
  945.     }
  946.     patchDay, err := time.Parse("2006-01-02 15:04" /*Siege Green Patch Time:*/, "2022-05-10 12:00")
  947.     if err != nil {
  948.         fmt.Println(err)
  949.         return
  950.     }
  951.     c := NewCoordinator(patchDay,
  952.         ships,
  953.         zkc,
  954.         context.Background())
  955.     as, pkms := c.Do()
  956.     err = as.Write(*outAggFile, ships)
  957.     if err != nil {
  958.         fmt.Println(err)
  959.     }
  960.     err = WriteParsedKillmails(pkms, *srcDataFile)
  961.     if err != nil {
  962.         fmt.Println(err)
  963.     }
  964. }
  965.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement