Last active
July 6, 2019 20:02
-
-
Save sneakyness/e14965c3cd171a5c9fa8f74769a69895 to your computer and use it in GitHub Desktop.
Used http://app.quicktype.io to generate Swift Models for stats.quake.com API. All 3 routes (Leaderboards/Player Stats/Match History) work and serialize into codable model objects. Quad Thanks to Sponge for being very helpful and prompt with the questions I had while piecing this together. Twitter thread of progress here: https://twitter.com/Sne…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum ChampionString: String, Codable { | |
case ranger = "RANGER" | |
case scalebearer = "SCALEBEARER" | |
case visor = "VISOR" | |
case anarki = "ANARKI" | |
case nyx = "NYX" | |
case sorlag = "SORLAG" | |
case clutch = "CLUTCH" | |
case galena = "GALENA" | |
case slash = "SLASH" | |
case doomslayer = "DOOM_SLAYER" | |
case bjblazkowicz = "BJ_BLAZKOWICZ" | |
case keel = "KEEL" | |
case strogg = "STROGG" | |
case deathknight = "DEATH_KNIGHT" | |
} | |
struct PlayerStats: Codable { // top level object for …/player/Stats | |
let name: String | |
let playerRatings: PlayerRatings | |
let playerLoadOut: PlayerLoadOut | |
let playerProfileStats: PlayerProfileStats | |
let playerLevelState: PlayerLevelState | |
let matches: [MatchStats] | |
} | |
struct MatchStats: Codable { | |
let id, playedDateTime: String | |
let gameMode, map, score: JSONNull? | |
let scoreLimit, timeLimit: Int | |
} | |
struct GamesSummary: Codable { // top level object for …/player/GamesSummary | |
let matches: [MatchSummary] | |
} | |
struct MatchSummary: Codable { | |
let id, time, mapName: String | |
let rank: Int | |
let score: [Int]? | |
let gameMode: GameModeString | |
let won: Bool | |
let xp: Int | |
let kdr: Double | |
let totalDamage: Int | |
let weaponAccuracy: [String: Double] | |
} | |
struct Leaderboards: Codable { // top level object for …/Leaderboard | |
let boardType: String | |
let entries: [LeaderboardEntry] | |
let totalEntries: Int | |
} | |
struct LeaderboardEntry: Codable { | |
let userName: String | |
let eloRating: Int | |
let profileIconID, namePlateID: String | |
enum CodingKeys: String, CodingKey { | |
case userName, eloRating | |
case profileIconID = "profileIconId" | |
case namePlateID = "namePlateId" | |
} | |
} | |
typealias PlayerSearch = [PlayerSearchElement] // top level object for player search | |
struct PlayerSearchElement: Codable { | |
let entityID, entityName: String | |
enum CodingKeys: String, CodingKey { | |
case entityID = "entityId" | |
case entityName | |
} | |
} | |
struct PlayerLevelState: Codable { | |
let level, exp: Int | |
} | |
struct PlayerLoadOut: Codable { | |
let namePlateID, iconID: String | |
enum CodingKeys: String, CodingKey { | |
case namePlateID = "namePlateId" | |
case iconID = "iconId" | |
} | |
} | |
struct PlayerProfileStats: Codable { | |
let champions: [String: ChampionStats] | |
} | |
struct ChampionStats: Codable { | |
let gameModes: GameMode | |
let damageStatusList: [String: DamageStatusList] | |
// let medals: JSONNull? // sponge said this is dead, safe to remove | |
} | |
struct DamageStatusList: Codable { | |
let hits, shots, kills, headshots: Int | |
let damage: Int | |
} | |
enum GameModeString: String, Codable { | |
case ffa = "FFA" | |
case tdm = "TDM" | |
case duel = "DUEL" | |
case sacrifice = "SACRIFICE" | |
case sacrificePro = "SACRIFICE_PRO" | |
case tdm2Vs2 = "TDM_2VS2" | |
case instagib = "INSTAGIB" | |
case duelPro = "DUEL_PRO" | |
} | |
struct GameMode: Codable { | |
let ffa, tdm, duel, sacrifice: GameModeStats | |
let sacrificePro, tdm2Vs2, instagib, duelPro: GameModeStats | |
enum CodingKeys: String, CodingKey { | |
case ffa = "FFA" | |
case tdm = "TDM" | |
case duel = "DUEL" | |
case sacrifice = "SACRIFICE" | |
case sacrificePro = "SACRIFICE_PRO" | |
case tdm2Vs2 = "TDM_2VS2" | |
case instagib = "INSTAGIB" | |
case duelPro = "DUEL_PRO" | |
} | |
} | |
struct GameModeStats: Codable { | |
let won, lost, tie, lifeTime: Int | |
let timePlayed, kills, deaths, powerPickups: Int | |
let megaHealthPickups, heavyArmorPickups, tacticalPickups, score: Int | |
let captured, defended: Int | |
let scoringEvents: [String: Int] | |
let healed, smallArmorPickups, rankedWon, rankedLost: Int | |
let rankedTied, rankedTimePlayed: Int | |
} | |
struct PlayerRatings: Codable { | |
let duel, tdm: GameModeRating | |
} | |
struct GameModeRating: Codable { | |
let rating, deviation: Int | |
let volitility: Double | |
let lastUpdated, gamesCount, lastChange: Int | |
let history: [RatingHistory] | |
} | |
struct RatingHistory: Codable { | |
let gamesPlayed, eloRating: Int | |
let time: String | |
let result: Int | |
let sessionID: String | |
enum CodingKeys: String, CodingKey { | |
case gamesPlayed, eloRating, time, result | |
case sessionID = "sessionId" | |
} | |
} | |
struct GameSearch: Codable { // top level item for game search | |
let summary: GameSummary // null if no player provided | |
let id: String | |
let teamScores: [JSONAny] | |
let playedDateTime, mapName: String | |
let timeLimit, scoreLimit: Int | |
let gameMode: String | |
let battleReportPersonalStatistics: [BattleReportPersonalStatistic] | |
} | |
struct BattleReportPersonalStatistic: Codable { | |
let isCustomGame: Bool | |
let userID, nickname: String | |
let teamIndex, averageLifetime, score, totalDamage: Int | |
let kills, deaths, previousProfileXP, megaHealthPickups: Int | |
let heavyArmorPickups, smallArmorPickups, powerPickups, hpHealed: Int | |
let bestWeapon: String | |
let bestWeaponAccuracyPercent, bestWeaponDamage, bestWeaponKills: Int | |
let lastSelectedChampion: String | |
let completedChallenges: [Int] | |
let completedStaticChallenges: [JSONAny] | |
let scoringEvents: [String: Int] | |
let championsTime: ChampionsTime | |
let frag, duelScore: Int | |
let squadChampionIndexArray: [JSONAny] | |
let damageStatsWithDamageTypeIndexArray: [String: SumAllDamageStats] | |
let sumAllDamageStats: SumAllDamageStats | |
let earnedXP, bonusXP, earnedFavor, bonusFavor: Int | |
enum CodingKeys: String, CodingKey { | |
case isCustomGame | |
case userID = "userId" | |
case nickname, teamIndex, averageLifetime, score, totalDamage, kills, deaths | |
case previousProfileXP = "previousProfileXp" | |
case megaHealthPickups, heavyArmorPickups, smallArmorPickups, powerPickups, hpHealed, bestWeapon, bestWeaponAccuracyPercent, bestWeaponDamage, bestWeaponKills, lastSelectedChampion, completedChallenges, completedStaticChallenges, scoringEvents, championsTime, frag, duelScore, squadChampionIndexArray, damageStatsWithDamageTypeIndexArray, sumAllDamageStats | |
case earnedXP = "earnedXp" | |
case bonusXP = "bonusXp" | |
case earnedFavor, bonusFavor | |
} | |
} | |
struct ChampionsTime: Codable { | |
let galena, sorlag, scalebearer, anarki: Int? | |
let nyx, keel, deathknight, visor: Int? | |
let ranger, clutch, slash, doomslayer: Int? | |
let bjblazkowicz, strogg: Int? | |
enum CodingKeys: String, CodingKey { | |
case ranger = "RANGER" | |
case scalebearer = "SCALEBEARER" | |
case visor = "VISOR" | |
case anarki = "ANARKI" | |
case nyx = "NYX" | |
case sorlag = "SORLAG" | |
case clutch = "CLUTCH" | |
case galena = "GALENA" | |
case slash = "SLASH" | |
case doomslayer = "DOOM_SLAYER" | |
case bjblazkowicz = "BJ_BLAZKOWICZ" | |
case keel = "KEEL" | |
case strogg = "STROGG" | |
case deathknight = "DEATH_KNIGHT" | |
} | |
} | |
struct SumAllDamageStats: Codable { | |
let shots, hits, damage, kills: Int | |
} | |
struct GameSummary: Codable { | |
let id, time, mapName: String | |
let rank: Int | |
let score: JSONNull? | |
let gameMode: String | |
let won: Bool | |
let xp: Int | |
let kdr: Double | |
let totalDamage: Int | |
let weaponAccuracy: [String: Double] | |
} | |
// MARK: - URLSession response handlers | |
extension URLSession { | |
fileprivate func codableTask<T: Codable>(with url: URL, completionHandler: @escaping (T?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.dataTask(with: url) { data, response, error in | |
guard let data = data, error == nil else { | |
completionHandler(nil, response, error) | |
return | |
} | |
completionHandler(try? JSONDecoder().decode(T.self, from: data), response, nil) | |
} | |
} | |
func playerStatsTask(with url: URL, completionHandler: @escaping (PlayerStats?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.codableTask(with: url, completionHandler: completionHandler) | |
} | |
func gamesSummaryTask(with url: URL, completionHandler: @escaping (GamesSummary?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.codableTask(with: url, completionHandler: completionHandler) | |
} | |
func leaderboardsTask(with url: URL, completionHandler: @escaping (Leaderboards?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.codableTask(with: url, completionHandler: completionHandler) | |
} | |
func playerSearchTask(with url: URL, completionHandler: @escaping (PlayerSearch?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.codableTask(with: url, completionHandler: completionHandler) | |
} | |
func gameSearchTask(with url: URL, completionHandler: @escaping (GameSearch?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
return self.codableTask(with: url, completionHandler: completionHandler) | |
} | |
} | |
// MARK: Encode/decode helpers | |
class JSONNull: Codable { | |
public init() {} | |
public required init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if !container.decodeNil() { | |
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try container.encodeNil() | |
} | |
} | |
class JSONCodingKey: CodingKey { | |
let key: String | |
required init?(intValue: Int) { | |
return nil | |
} | |
required init?(stringValue: String) { | |
key = stringValue | |
} | |
var intValue: Int? { | |
return nil | |
} | |
var stringValue: String { | |
return key | |
} | |
} | |
class JSONAny: Codable { | |
let value: Any | |
static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") | |
return DecodingError.typeMismatch(JSONAny.self, context) | |
} | |
static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { | |
let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") | |
return EncodingError.invalidValue(value, context) | |
} | |
static func decode(from container: SingleValueDecodingContainer) throws -> Any { | |
if let value = try? container.decode(Bool.self) { | |
return value | |
} | |
if let value = try? container.decode(Int64.self) { | |
return value | |
} | |
if let value = try? container.decode(Double.self) { | |
return value | |
} | |
if let value = try? container.decode(String.self) { | |
return value | |
} | |
if container.decodeNil() { | |
return JSONNull() | |
} | |
throw decodingError(forCodingPath: container.codingPath) | |
} | |
static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { | |
if let value = try? container.decode(Bool.self) { | |
return value | |
} | |
if let value = try? container.decode(Int64.self) { | |
return value | |
} | |
if let value = try? container.decode(Double.self) { | |
return value | |
} | |
if let value = try? container.decode(String.self) { | |
return value | |
} | |
if let value = try? container.decodeNil() { | |
if value { | |
return JSONNull() | |
} | |
} | |
if var container = try? container.nestedUnkeyedContainer() { | |
return try decodeArray(from: &container) | |
} | |
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { | |
return try decodeDictionary(from: &container) | |
} | |
throw decodingError(forCodingPath: container.codingPath) | |
} | |
static func decode(from container: inout KeyedDecodingContainer<JSONCodingKey>, forKey key: JSONCodingKey) throws -> Any { | |
if let value = try? container.decode(Bool.self, forKey: key) { | |
return value | |
} | |
if let value = try? container.decode(Int64.self, forKey: key) { | |
return value | |
} | |
if let value = try? container.decode(Double.self, forKey: key) { | |
return value | |
} | |
if let value = try? container.decode(String.self, forKey: key) { | |
return value | |
} | |
if let value = try? container.decodeNil(forKey: key) { | |
if value { | |
return JSONNull() | |
} | |
} | |
if var container = try? container.nestedUnkeyedContainer(forKey: key) { | |
return try decodeArray(from: &container) | |
} | |
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { | |
return try decodeDictionary(from: &container) | |
} | |
throw decodingError(forCodingPath: container.codingPath) | |
} | |
static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { | |
var arr: [Any] = [] | |
while !container.isAtEnd { | |
let value = try decode(from: &container) | |
arr.append(value) | |
} | |
return arr | |
} | |
static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] { | |
var dict = [String: Any]() | |
for key in container.allKeys { | |
let value = try decode(from: &container, forKey: key) | |
dict[key.stringValue] = value | |
} | |
return dict | |
} | |
static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { | |
for value in array { | |
if let value = value as? Bool { | |
try container.encode(value) | |
} else if let value = value as? Int64 { | |
try container.encode(value) | |
} else if let value = value as? Double { | |
try container.encode(value) | |
} else if let value = value as? String { | |
try container.encode(value) | |
} else if value is JSONNull { | |
try container.encodeNil() | |
} else if let value = value as? [Any] { | |
var container = container.nestedUnkeyedContainer() | |
try encode(to: &container, array: value) | |
} else if let value = value as? [String: Any] { | |
var container = container.nestedContainer(keyedBy: JSONCodingKey.self) | |
try encode(to: &container, dictionary: value) | |
} else { | |
throw encodingError(forValue: value, codingPath: container.codingPath) | |
} | |
} | |
} | |
static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws { | |
for (key, value) in dictionary { | |
let key = JSONCodingKey(stringValue: key)! | |
if let value = value as? Bool { | |
try container.encode(value, forKey: key) | |
} else if let value = value as? Int64 { | |
try container.encode(value, forKey: key) | |
} else if let value = value as? Double { | |
try container.encode(value, forKey: key) | |
} else if let value = value as? String { | |
try container.encode(value, forKey: key) | |
} else if value is JSONNull { | |
try container.encodeNil(forKey: key) | |
} else if let value = value as? [Any] { | |
var container = container.nestedUnkeyedContainer(forKey: key) | |
try encode(to: &container, array: value) | |
} else if let value = value as? [String: Any] { | |
var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) | |
try encode(to: &container, dictionary: value) | |
} else { | |
throw encodingError(forValue: value, codingPath: container.codingPath) | |
} | |
} | |
} | |
static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { | |
if let value = value as? Bool { | |
try container.encode(value) | |
} else if let value = value as? Int64 { | |
try container.encode(value) | |
} else if let value = value as? Double { | |
try container.encode(value) | |
} else if let value = value as? String { | |
try container.encode(value) | |
} else if value is JSONNull { | |
try container.encodeNil() | |
} else { | |
throw encodingError(forValue: value, codingPath: container.codingPath) | |
} | |
} | |
public required init(from decoder: Decoder) throws { | |
if var arrayContainer = try? decoder.unkeyedContainer() { | |
self.value = try JSONAny.decodeArray(from: &arrayContainer) | |
} else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { | |
self.value = try JSONAny.decodeDictionary(from: &container) | |
} else { | |
let container = try decoder.singleValueContainer() | |
self.value = try JSONAny.decode(from: container) | |
} | |
} | |
public func encode(to encoder: Encoder) throws { | |
if let arr = self.value as? [Any] { | |
var container = encoder.unkeyedContainer() | |
try JSONAny.encode(to: &container, array: arr) | |
} else if let dict = self.value as? [String: Any] { | |
var container = encoder.container(keyedBy: JSONCodingKey.self) | |
try JSONAny.encode(to: &container, dictionary: dict) | |
} else { | |
var container = encoder.singleValueContainer() | |
try JSONAny.encode(to: &container, value: self.value) | |
} | |
} | |
} | |
// TODO: scoring event constants | |
//"SCORING_EVENT_KILL": 2094, | |
//"SCORING_EVENT_ASSIST": 0, | |
//"SCORING_EVENT_HEADSHOT": 28, | |
//"SCORING_EVENT_MIDAIRKILL": 48, | |
//"SCORING_EVENT_IMPRESSIVE": 81, | |
//"SCORING_EVENT_DIEHARD": 7, | |
//"SCORING_EVENT_FIRSTBLOOD": 10, | |
//"SCORING_EVENT_ABILITYKILL": 218, | |
//"SCORING_EVENT_EXCELENT": 164, | |
//"SCORING_EVENT_TRIPLEKILL": 13, | |
//"SCORING_EVENT_OVERKILL": 2, | |
//"SCORING_EVENT_EXTIRPATOR": 1, | |
//"SCORING_EVENT_CRUSHER": 1, | |
//"SCORING_EVENT_EXTERMINATOR": 1, | |
//"SCORING_EVENT_DESTROYER": 0, | |
//"SCORING_EVENT_REAPER": 0, | |
//"SCORING_EVENT_GOD_OF_DEATH": 0, | |
//"SCORING_EVENT_HITMAN": 0, | |
//"SCORING_EVENT_GAUNTLET": 56, | |
//"SCORING_EVENT_COMBO_KILL": 3, | |
//"SCORING_EVENT_RING_OUT": 17, | |
//"SCORING_EVENT_PERFORATION": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_1": 62, | |
//"SCORING_EVENT_SERIAL_KILLER_2": 2, | |
//"SCORING_EVENT_SERIAL_KILLER_3": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_4": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_5": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_6": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_7": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_8": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_9": 0, | |
//"SCORING_EVENT_SERIAL_KILLER_10": 0, | |
//"SCORING_EVENT_LAST_SHOT": 0, | |
//"SCORING_EVENT_ASSISTANT": 0, | |
//"SCORING_EVENT_STOP": 0, | |
//"SCORING_EVENT_GUARDIAN": 0, | |
//"SCORING_EVENT_SNIPER": 0, | |
//"SCORING_EVENT_REVENGE": 103, | |
//"SCORING_EVENT_CLOWN": 0, | |
//"SCORING_EVENT_FROM_THE_GRAVE": 30, | |
//"SCORING_EVENT_BAD_LUCK": 0, | |
//"SCORING_EVENT_COUNTER_ABILITY": 0, | |
//"SCORING_EVENT_MELEE_MASTERY": 0, | |
//"SCORING_EVENT_MELEE_GOD": 0, | |
//"SCORING_EVENT_MACHINEGUN_MASTERY": 0, | |
//"SCORING_EVENT_MACHINEGUN_GOD": 0, | |
//"SCORING_EVENT_SHOTGUN_MASTERY": 0, | |
//"SCORING_EVENT_SHOTGUN_GOD": 1, | |
//"SCORING_EVENT_ROCKET_LAUNCHER_MASTERY": 0, | |
//"SCORING_EVENT_ROCKET_LAUNCHER_GOD": 11, | |
//"SCORING_EVENT_LIGHTNING_GUN_MASTERY": 0, | |
//"SCORING_EVENT_LIGHTNING_GUN_GOD": 0, | |
//"SCORING_EVENT_RAILGUN_MASTERY": 0, | |
//"SCORING_EVENT_RAILGUN_GOD": 0, | |
//"SCORING_EVENT_NAILGUN_MASTERY": 0, | |
//"SCORING_EVENT_NAILGUN_GOD": 0, | |
//"SCORING_EVENT_FIGHTER": 0, | |
//"SCORING_EVENT_TERRORIST": 0, | |
//"SCORING_EVENT_AVENGER": 0, | |
//"SCORING_EVENT_QUAD_KILLER": 0, | |
//"SCORING_EVENT_RAGE": 0, | |
//"SCORING_EVENT_FLOCK": 0, | |
//"SCORING_EVENT_DAMAGE_DEALER": 51, | |
//"SCORING_EVENT_TELEFRAG": 2, | |
//"SCORING_EVENT_OBELISK_CAPTURE": 0, | |
//"SCORING_EVENT_OBELISK_CAPTURE_ASSIST": 0, | |
//"SCORING_EVENT_OBELISK_DEFEND": 0, | |
//"SCORING_EVENT_POWERUP": 102, | |
//"SCORING_EVENT_FFA_PERFECT": 0, | |
//"SCORING_EVENT_OBELISK_MASTERY": 0, | |
//"SCORING_EVENT_OBELISK_OFFENCE": 0, | |
//"SCORING_EVENT_MATCH_WIN": 20, | |
//"SCORING_EVENT_MATCH_COMPLETE": 61, | |
//"SCORING_EVENT_TEAM_WIN": 0, | |
//"SCORING_EVENT_TEAM_TIE": 0, | |
//"SCORING_EVENT_1_PLACE": 20, | |
//"SCORING_EVENT_2_PLACE": 14, | |
//"SCORING_EVENT_3_PLACE": 12, | |
//"SCORING_EVENT_MVP": 0, | |
//"SCORING_EVENT_DAMAGE": 3863, | |
//"SCORING_EVENT_ABILITY_HEAL": 0, | |
//"SCORING_EVENT_BLOCKING": 0, | |
//"SCORING_EVENT_PICKUP_MAJOR": 385, | |
//"SCORING_EVENT_PICKUP_WEAPON": 3154, | |
//"SCORING_EVENT_TOTEM_DESTROY": 39, | |
//"SCORING_EVENT_ABILITY_USE_RANGER": 1113, | |
//"SCORING_EVENT_FIRST_GAME": 0, | |
//"SCORING_EVENT_FIRST_WIN": 0, | |
//"SCORING_EVENT_ABILITY_USE_VISOR": 0, | |
//"SCORING_EVENT_ABILITY_USE_SCALEBEARER": 0, | |
//"SCORING_EVENT_ABILITY_USE_NYX": 0, | |
//"SCORING_EVENT_ABILITY_USE_GALENA": 0, | |
//"SCORING_EVENT_ABILITY_USE_ANARKI": 0, | |
//"SCORING_EVENT_ABILITY_USE_CLUTCH": 0, | |
//"SCORING_EVENT_ABILITY_USE_SLASH": 0, | |
//"SCORING_EVENT_ABILITY_USE_SORLAG": 0, | |
//"SCORING_EVENT_ABILITY_USE_KEEL": 0, | |
//"SCORING_EVENT_ABILITY_USE_STROGG": 0, | |
//"SCORING_EVENT_ABILITY_USE_DOOM": 0, | |
//"SCORING_EVENT_ABILITY_USE_BJ": 0, | |
//"SCORING_EVENT_HELPING_HAND": 0, | |
//"SCORING_EVENT_QUATERBACK": 0, | |
//"SCORING_EVENT_DEFENDER": 0, | |
//"SCORING_EVENT_SURVIVOR": 10, | |
//"SCORING_EVENT_SCAVENGER": 20, | |
//"SCORING_EVENT_MOST_DAMAGE": 27, | |
//"SCORING_EVENT_MOST_ACCURATE": 10, | |
//"SCORING_EVENT_PARTY_BREAKER": 43, | |
//"SCORING_EVENT_DENIED": 98, | |
//"SCORING_EVENT_POWERUP_MASSACRE": 6, | |
//"SCORING_EVENT_POINT_BLANK": 83, | |
//"SCORING_EVENT_NET_MASTER": 20, | |
//"SCORING_EVENT_TANK": 125, | |
//"SCORING_EVENT_SHOWSTOPPER": 44, | |
//"SCORING_EVENT_OT": 0, | |
//"SCORING_EVENT_COMEBACK": 0, | |
//"SCORING_EVENT_PRECISE": 4, | |
//"SCORING_EVENT_COLLATERAL_DAMAGE": 1, | |
//"SCORING_EVENT_DP": 0, | |
//"SCORING_EVENT_STOPWATCH": 72, | |
//"SCORING_EVENT_NEMESIS": 0, | |
//"SCORING_EVENT_AIRBORNE": 27, | |
//"SCORING_EVENT_BRING_EM_DOWN": 0, | |
//"SCORING_EVENT_DOMINATING": 3, | |
//"SCORING_EVENT_PROVACATEUR": 0, | |
//"SCORING_EVENT_KAMIKAZE": 10, | |
//"SCORING_EVENT_DISINTEGRATOR": 2, | |
//"SCORING_EVENT_SHUB_SLAYER": 55, | |
//"SCORING_EVENT_DEADLY_SLIPGATE": 43, | |
//"SCORING_EVENT_SHOOTING_RANGE": 0, | |
//"SCORING_EVENT_CONTROLLER": 0, | |
//"SCORING_EVENT_SEER": 0, | |
//"SCORING_EVENT_STOMPER": 0, | |
//"SCORING_EVENT_CHOOCHOO": 0, | |
//"SCORING_EVENT_ROADKILL": 0, | |
//"SCORING_EVENT_EVASIVE": 0, | |
//"SCORING_EVENT_INSIDEOUT": 0, | |
//"SCORING_EVENT_FROM_THE_SHADOW": 0, | |
//"SCORING_EVENT_HOVERTANK": 0, | |
//"SCORING_EVENT_REANIMATION": 0, | |
//"SCORING_EVENT_JUNKIE": 0, | |
//"SCORING_EVENT_PLAGUE": 0, | |
//"SCORING_EVENT_MELTDOWN": 0, | |
//"SCORING_EVENT_VENOMOUS": 0, | |
//"SCORING_EVENT_THEROCK": 0, | |
//"SCORING_EVENT_THEDRILL": 0, | |
//"SCORING_EVENT_THESHOCKER": 0, | |
//"SCORING_EVENT_DAMNED": 0, | |
//"SCORING_EVENT_BLESSED": 0, | |
//"SCORING_EVENT_CURSED": 0, | |
//"SCORING_EVENT_NAZI_SLAYER": 0, | |
//"SCORING_EVENT_MEIN_LEBEN": 0, | |
//"SCORING_EVENT_GET_PSYCHED": 0, | |
//"SCORING_EVENT_SLIDER": 0, | |
//"SCORING_EVENT_SLALOM": 0, | |
//"SCORING_EVENT_BLACK_SWAN": 0, | |
//"SCORING_EVENT_BERSERKER": 0, | |
//"SCORING_EVENT_DAISY": 0, | |
//"SCORING_EVENT_SLAYER": 0, | |
//"SCORING_EVENT_TRIBOLT_GOD": 0, | |
//"SCORING_EVENT_PINEAPPLE": 0, | |
//"SCORING_EVENT_NADE_SPAM": 0, | |
//"SCORING_EVENT_MORTAR": 0, | |
//"SCORING_EVENT_BOMBARDMENT": 1, | |
//"SCORING_EVENT_PSI_RADAR": 10, | |
//"SCORING_EVENT_AIR_ASSAULT": 0, | |
//"SPECIAL_DAMAGE_DEALER_EVENT": 0, | |
//"SPECIAL_HEALER_EVENT": 1, | |
//"SPECIAL_DAMAGE_ABSORBED_EVENT": 0, | |
//"CHAMPION_MATCH_WIN": 20, | |
//"CHAMPION_MATCH_COMPLETE": 50, | |
//"ABILLITY_KILLS_GOLD": 0, | |
//"ABILLITY_KILLS_SILVER": 0, | |
//"ABILLITY_KILLS_BRONZE": 0, | |
//"DAMAGE_DEALT_GOLD": 0, | |
//"DAMAGE_DEALT_SILVER": 0, | |
//"DAMAGE_DEALT_BRONZE": 0, | |
//"ASSIST_GOLD": 0, | |
//"ASSIST_SILVER": 0, | |
//"ASSIST_BRONZE": 0, | |
//"MACHINEGUN_KILLS_GOLD": 0, | |
//"MACHINEGUN_KILLS_SILVER": 0, | |
//"MACHINEGUN_KILLS_BRONZE": 0, | |
//"RAILGUN_KILLS_GOLD": 0, | |
//"RAILGUN_KILLS_SILVER": 0, | |
//"RAILGUN_KILLS_BRONZE": 0, | |
//"ROCKET_KILLS_GOLD": 0, | |
//"ROCKET_KILLS_SILVER": 0, | |
//"ROCKET_KILLS_BRONZE": 0, | |
//"NAILGUN_KILLS_GOLD": 0, | |
//"NAILGUN_KILLS_SILVER": 0, | |
//"NAILGUN_KILLS_BRONZE": 0, | |
//"TRIBOLT_KIILS_GOLD": 0, | |
//"TRIBOLT_KIILS_SILVER": 0, | |
//"TRIBOLT_KIILS_BRONZE": 0, | |
//"SHOTGUN_KILLS_GOLD": 0, | |
//"SHOTGUN_KILLS_SILVER": 0, | |
//"SHOTGUN_KILLS_BRONZE": 0, | |
//"LIGHTNING_GUN_KILLS_GOLD": 0, | |
//"LIGHTNING_GUN_KILLS_SILVER": 0, | |
//"LIGHTNING_GUN_KILLS_BRONZE": 0, | |
//"ACCURACY_GOLD": 0, | |
//"ACCURACY_SILVER": 0, | |
//"ACCURACY_BRONZE": 0, | |
//"GAUNTLET_KILLS_GOLD": 0, | |
//"GAUNTLET_KILLS_SILVER": 0, | |
//"GAUNTLET_KILLS_BRONZE": 0, | |
//"KILLS_BASIC": 0, | |
//"DAMAGE_DEALT_BASIC": 0, | |
//"ASSIST_BASIC": 0, | |
//"HEAL_BASIC": 0, | |
//"ARMOR_PICKUP_BASIC": 0, | |
//"MEGA_PICKUP_BASIC": 0, | |
//"POWER_PICKUP_BASIC": 0, | |
//"SCORING_EVENT_INFERNO": 0, | |
//"SCORING_EVENT_TRIDENT": 0, | |
//"SCORING_EVENT_SMELTER": 0, | |
//"SCORING_EVENT_PICKUP_MEGA_HEALTH": 124, | |
//"SCORING_EVENT_PICKUP_HEAVY_ARMOR": 141, | |
//"SCORING_EVENT_ABILITY_USE_ANY": 743 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Routes | |
//https://stats.quake.com/api/v2/Leaderboard?from=0&board={duel,tdm}&season={current,preseason1} | |
//https://stats.quake.com/api/v2/Player/Stats?name=sneakyness | |
//https://stats.quake.com/api/v2/Player/GamesSummary?name=sneakyness | |
//https://stats.quake.com/api/v2/Player/Search?term=sneakyness | |
//https://stats.quake.com/api/v2/Player/Games?playerName=sneakyness&id=43291c80-9aaa-11e8-bce6-0003ffb6d7a2, where id is the id of a MatchSummary (playerName optional) | |
guard let url = URL(string: "https://stats.quake.com/api/v2/Leaderboard?from=0&board=tdm&season=current") else {return} | |
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in | |
guard let dataResponse = data, | |
error == nil else { | |
print(error?.localizedDescription ?? "Response Error") | |
return } | |
do { | |
//here dataResponse received from a network request | |
let jsonResponse = try JSONSerialization.jsonObject(with: | |
dataResponse, options: []) | |
// debugPrint(jsonResponse) | |
// print("\n ---- \n") | |
// let playerStats = try? JSONDecoder().decode(PlayerStats.self, from: jsonData) | |
// let gamesSummary = try? JSONDecoder().decode(GamesSummary.self, from: jsonData) | |
// let leaderboards = try? JSONDecoder().decode(Leaderboards.self, from: jsonData) | |
let leaderboards = try JSONDecoder().decode(Leaderboards.self, from: dataResponse) | |
// debugPrint(summary) | |
} catch let parsingError { | |
print("Error", parsingError) | |
} | |
} | |
task.resume() |
Thank you so much @aleab! I'll toss those in shortly
Oh– forgot to specify that the playerName
parameter is actually optional.
Player name search and game search have been added to the gist. I've only tested name search so far, game search is thicc so I'm gonna loop back around to that tomorrow. Thanks again 👍
⭐ Don't forget to star this gist if you find it useful ⭐
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In case you missed them, there are also these two endpoints in the API:
Player/Search?term=
Player/Games?playerName=&id=
, whereid
is the id of aMatchSummary