├── .editorconfig ├── .gitignore ├── Directory.Build.props ├── LICENSE.md ├── README.md ├── SpeedrunComSharp.sln ├── props ├── SpeedrunComSharp.Paths.props └── SpeedrunComSharp.props └── src └── SpeedrunComSharp ├── APIException.cs ├── CachedEnumerable.cs ├── Categories ├── CategoriesClient.cs ├── CategoriesOrdering.cs ├── Category.cs ├── CategoryEmbeds.cs ├── CategoryType.cs └── Players.cs ├── Common ├── Assets.cs ├── ImageAsset.cs ├── Moderator.cs ├── ModeratorType.cs ├── Player.cs ├── PlayersType.cs └── TimingMethod.cs ├── ElementDescription.cs ├── ElementType.cs ├── Embeds.cs ├── Games ├── Game.cs ├── GameEmbeds.cs ├── GameHeader.cs ├── GamesClient.cs ├── GamesOrdering.cs └── Ruleset.cs ├── Guests ├── Guest.cs └── GuestsClient.cs ├── HttpWebLink.cs ├── IElementWithID.cs ├── JSON.cs ├── Leaderboards ├── EmulatorsFilter.cs ├── Leaderboard.cs ├── LeaderboardEmbeds.cs ├── LeaderboardScope.cs ├── LeaderboardsClient.cs └── Record.cs ├── Levels ├── Level.cs ├── LevelEmbeds.cs ├── LevelsClient.cs └── LevelsOrdering.cs ├── NotAuthorizedException.cs ├── Notifications ├── Notification.cs ├── NotificationStatus.cs ├── NotificationType.cs ├── NotificationsClient.cs └── NotificationsOrdering.cs ├── Platforms ├── Platform.cs ├── PlatformsClient.cs └── PlatformsOrdering.cs ├── PotentialEmbed.cs ├── Regions ├── Region.cs ├── RegionsClient.cs └── RegionsOrdering.cs ├── Runs ├── Run.cs ├── RunEmbeds.cs ├── RunStatus.cs ├── RunStatusType.cs ├── RunSystem.cs ├── RunTimes.cs ├── RunVideos.cs ├── RunsClient.cs └── RunsOrdering.cs ├── Series ├── Series.cs ├── SeriesClient.cs ├── SeriesEmbeds.cs └── SeriesOrdering.cs ├── SpeedrunComClient.cs ├── SpeedrunComSharp.csproj ├── StringHelpers.cs ├── Users ├── Country.cs ├── CountryRegion.cs ├── Location.cs ├── User.cs ├── UserNameStyle.cs ├── UserRole.cs ├── UsersClient.cs └── UsersOrdering.cs └── Variables ├── Variable.cs ├── VariableScope.cs ├── VariableScopeType.cs ├── VariableValue.cs ├── VariablesClient.cs └── VariablesOrdering.cs /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE options/cache directories 2 | .vs/ 3 | .vscode/ 4 | .idea/ 5 | .fleet/ 6 | 7 | # Build artifacts 8 | artifacts/ 9 | bin/ 10 | obj/ 11 | publish/ 12 | 13 | # User-specific files 14 | *.user 15 | 16 | # MSBuild 17 | *.binlog 18 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christopher Serr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpeedrunComSharp 2 | 3 | [![API Version](https://img.shields.io/badge/API-c4413b...-blue.svg)](https://github.com/speedruncom/api/tree/c4413b86d7c2088c317de8e3940b022b11c26323) 4 | 5 | SpeedrunComSharp is a .NET wrapper Library for the [Speedrun.com API](https://github.com/speedruncom/api). 6 | 7 | ## How to use 8 | 9 | Download and compile the library and add it as a reference to your project. You then need to create an object of the `SpeedrunComClient` like so: 10 | 11 | ```C# 12 | var client = new SpeedrunComClient(); 13 | ``` 14 | 15 | The Client is separated into the following Sub-Clients, just like the Speedrun.com API: 16 | * Categories 17 | * Games 18 | * Guests 19 | * Leaderboards 20 | * Levels 21 | * Notifications 22 | * Platforms 23 | * Profile 24 | * Regions 25 | * Runs 26 | * Series 27 | * Users 28 | * Variables 29 | 30 | The Sub-Clients implement all the API Calls for retrieving the Objects from the API. Once you obtained objects from those Clients, you can either use the References within the Objects to retrieve additional objects or you can use their IDs to retrieve them through the Clients. 31 | 32 | ## Example Usage 33 | 34 | ```C# 35 | //Creating the Client 36 | var client = new SpeedrunComClient(); 37 | 38 | //Searching for a game called "Wind Waker" 39 | var game = client.Games.SearchGame(name: "Wind Waker"); 40 | 41 | //Printing all the categories of the game 42 | foreach (var category in game.Categories) 43 | { 44 | Console.WriteLine(category.Name); 45 | } 46 | 47 | //Searching for the category "Any%" 48 | var anyPercent = game.Categories.First(category => category.Name == "Any%"); 49 | 50 | //Finding the World Record of the category 51 | var worldRecord = anyPercent.WorldRecord; 52 | 53 | //Printing the World Record's information 54 | Console.WriteLine("The World Record is {0} by {1}", worldRecord.Times.Primary, worldRecord.Player.Name); 55 | 56 | ``` 57 | 58 | ## Optimizations 59 | 60 | The Clients are somewhat more flexible as the Properties in the individual Objects for traversing the API, because they can embed additional objects to decrease Network Usage. If you want to optimize your API usage, make sure to use the Clients where possible. 61 | 62 | The Library automatically minimizes Network Usage, so iterating over `category.Runs` multiple times for example only results in a single API Call. If you are iterating over an IEnumerable that results in a Paginated API Call, the API will only be called for those pages that you are iterating over. If two completely unrelated events result in the same API Call, the SpeedrunComSharp Library will notice that and return a cached result for the second API Call. 63 | -------------------------------------------------------------------------------- /SpeedrunComSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2DDA8DE7-9195-4278-9AB2-F660CBE900ED}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpeedrunComSharp", "src\SpeedrunComSharp\SpeedrunComSharp.csproj", "{49FD8C4A-8E54-440F-AC3A-571F9B99704F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {49FD8C4A-8E54-440F-AC3A-571F9B99704F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {49FD8C4A-8E54-440F-AC3A-571F9B99704F}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {49FD8C4A-8E54-440F-AC3A-571F9B99704F}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {49FD8C4A-8E54-440F-AC3A-571F9B99704F}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(NestedProjects) = preSolution 25 | {49FD8C4A-8E54-440F-AC3A-571F9B99704F} = {2DDA8DE7-9195-4278-9AB2-F660CBE900ED} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /props/SpeedrunComSharp.Paths.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory).. 5 | 6 | $(RootPath)\src 7 | $(RootPath)\test 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /props/SpeedrunComSharp.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 6 | 7 | true 8 | disable 9 | 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/APIException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace SpeedrunComSharp; 7 | 8 | public class APIException : ArgumentException 9 | { 10 | public ReadOnlyCollection Errors { get; } 11 | 12 | public APIException(string message) 13 | : this(message, new List().AsReadOnly()) 14 | { } 15 | 16 | public APIException(string message, IEnumerable errors) 17 | : base(message) 18 | { 19 | Errors = errors.ToList().AsReadOnly(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/CachedEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | internal static class CacheExtensions 6 | { 7 | internal static IEnumerable Cache(this IEnumerable enumerable) 8 | { 9 | return new CachedEnumerable(enumerable); 10 | } 11 | } 12 | 13 | internal class CachedEnumerable : IEnumerable 14 | { 15 | private readonly IEnumerable baseEnumerable; 16 | private IEnumerator baseEnumerator; 17 | private readonly List cachedElements; 18 | 19 | public CachedEnumerable(IEnumerable baseEnumerable) 20 | { 21 | this.baseEnumerable = baseEnumerable; 22 | cachedElements = []; 23 | } 24 | 25 | public IEnumerator GetEnumerator() 26 | { 27 | foreach (T element in cachedElements) 28 | { 29 | yield return element; 30 | } 31 | 32 | baseEnumerator ??= baseEnumerable.GetEnumerator(); 33 | 34 | while (baseEnumerator.MoveNext()) 35 | { 36 | T current = baseEnumerator.Current; 37 | cachedElements.Add(current); 38 | yield return current; 39 | } 40 | } 41 | 42 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 43 | { 44 | return GetEnumerator(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/CategoriesClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class CategoriesClient 8 | { 9 | public const string Name = "categories"; 10 | 11 | private readonly SpeedrunComClient baseClient; 12 | 13 | public CategoriesClient(SpeedrunComClient baseClient) 14 | { 15 | this.baseClient = baseClient; 16 | } 17 | 18 | public static Uri GetCategoriesUri(string subUri) 19 | { 20 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 21 | } 22 | 23 | /// 24 | /// Fetch a Category object identified by its URI. 25 | /// 26 | /// The site URI for the category. 27 | /// Optional. If included, will dictate the embedded resources included in the response. 28 | /// 29 | public Category GetCategoryFromSiteUri(string siteUri, CategoryEmbeds embeds = default) 30 | { 31 | string id = GetCategoryIDFromSiteUri(siteUri); 32 | 33 | if (string.IsNullOrEmpty(id)) 34 | { 35 | return null; 36 | } 37 | 38 | return GetCategory(id, embeds); 39 | } 40 | 41 | /// 42 | /// Fetch a Category ID identified by its URI. 43 | /// 44 | /// The site URI for the category. 45 | /// 46 | public string GetCategoryIDFromSiteUri(string siteUri) 47 | { 48 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 49 | 50 | if (elementDescription == null 51 | || elementDescription.Type != ElementType.Category) 52 | { 53 | return null; 54 | } 55 | 56 | return elementDescription.ID; 57 | } 58 | 59 | /// 60 | /// Fetch a Category object identified by its ID. 61 | /// 62 | /// The ID for the category. 63 | /// Optional. If included, will dictate the additional resources embedded in the response. 64 | /// 65 | public Category GetCategory(string categoryId, CategoryEmbeds embeds = default) 66 | { 67 | Uri uri = GetCategoriesUri(string.Format("/{0}{1}", Uri.EscapeDataString(categoryId), embeds.ToString().ToParameters())); 68 | dynamic result = baseClient.DoRequest(uri); 69 | 70 | return Category.Parse(baseClient, result.data); 71 | } 72 | 73 | /// 74 | /// Fetch a Collection of Variable objects from a category's ID. 75 | /// 76 | /// The ID for the category. 77 | /// Optional. If omitted, variables will be in the same order as the API. 78 | /// 79 | public ReadOnlyCollection GetVariables(string categoryId, 80 | VariablesOrdering orderBy = default) 81 | { 82 | var parameters = new List(orderBy.ToParameters()); 83 | 84 | Uri uri = GetCategoriesUri(string.Format("/{0}/variables{1}", 85 | Uri.EscapeDataString(categoryId), 86 | parameters.ToParameters())); 87 | 88 | return baseClient.DoDataCollectionRequest(uri, 89 | x => Variable.Parse(baseClient, x)); 90 | } 91 | 92 | /// 93 | /// Fetch a Leaderboard object from a category's ID. 94 | /// 95 | /// The ID for the category. 96 | /// Optional. If included, will dictate the amount of top runs included in the response. 97 | /// Optional. If included, will dictate whether or not empty leaderboards are included in the response. 98 | /// Optional. If included, will dictate the amount of elements included in each pagination. 99 | /// Optional. If included, will dictate the additional resources embedded in the response. 100 | /// 101 | public IEnumerable GetRecords(string categoryId, 102 | int? top = null, bool skipEmptyLeaderboards = false, 103 | int? elementsPerPage = null, 104 | LeaderboardEmbeds embeds = default) 105 | { 106 | var parameters = new List() { embeds.ToString() }; 107 | 108 | if (top.HasValue) 109 | { 110 | parameters.Add(string.Format("top={0}", top.Value)); 111 | } 112 | 113 | if (skipEmptyLeaderboards) 114 | { 115 | parameters.Add("skip-empty=true"); 116 | } 117 | 118 | if (elementsPerPage.HasValue) 119 | { 120 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 121 | } 122 | 123 | Uri uri = GetCategoriesUri(string.Format("/{0}/records{1}", 124 | Uri.EscapeDataString(categoryId), 125 | parameters.ToParameters())); 126 | 127 | return baseClient.DoPaginatedRequest(uri, 128 | x => Leaderboard.Parse(baseClient, x)); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/CategoriesOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Categories in responses. 7 | /// 8 | public enum CategoriesOrdering : int 9 | { 10 | Position = 0, 11 | PositionDescending, 12 | Name, 13 | NameDescending, 14 | Miscellaneous, 15 | MiscellaneousDescending 16 | } 17 | 18 | internal static class CategoriesOrderingHelpers 19 | { 20 | internal static IEnumerable ToParameters(this CategoriesOrdering ordering) 21 | { 22 | bool isDescending = ((int)ordering & 1) == 1; 23 | if (isDescending) 24 | { 25 | ordering = (CategoriesOrdering)((int)ordering - 1); 26 | } 27 | 28 | string str = ""; 29 | 30 | switch (ordering) 31 | { 32 | case CategoriesOrdering.Name: 33 | str = "name"; break; 34 | case CategoriesOrdering.Miscellaneous: 35 | str = "miscellaneous"; break; 36 | } 37 | 38 | var list = new List(); 39 | 40 | if (!string.IsNullOrEmpty(str)) 41 | { 42 | list.Add(string.Format("orderby={0}", str)); 43 | } 44 | 45 | if (isDescending) 46 | { 47 | list.Add("direction=desc"); 48 | } 49 | 50 | return list; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/Category.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class Category : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public string Name { get; private set; } 13 | public Uri WebLink { get; private set; } 14 | public CategoryType Type { get; private set; } 15 | public string Rules { get; private set; } 16 | public Players Players { get; private set; } 17 | public bool IsMiscellaneous { get; private set; } 18 | 19 | #region Links 20 | 21 | internal Lazy game; 22 | private Lazy> variables; 23 | private Lazy leaderboard; 24 | private Lazy worldRecord; 25 | 26 | public string GameID { get; private set; } 27 | public Game Game => game.Value; 28 | public ReadOnlyCollection Variables => variables.Value; 29 | public IEnumerable Runs { get; private set; } 30 | public Leaderboard Leaderboard => leaderboard.Value; 31 | public Record WorldRecord => worldRecord.Value; 32 | 33 | #endregion 34 | 35 | private Category() { } 36 | 37 | public static Category Parse(SpeedrunComClient client, dynamic categoryElement) 38 | { 39 | if (categoryElement is ArrayList) 40 | { 41 | return null; 42 | } 43 | 44 | var category = new Category 45 | { 46 | //Parse Attributes 47 | 48 | ID = categoryElement.id as string, 49 | Name = categoryElement.name as string, 50 | WebLink = new Uri(categoryElement.weblink as string), 51 | Type = categoryElement.type == "per-game" ? CategoryType.PerGame : CategoryType.PerLevel, 52 | Rules = categoryElement.rules as string, 53 | Players = Players.Parse(client, categoryElement.players), 54 | IsMiscellaneous = categoryElement.miscellaneous 55 | }; 56 | 57 | //Parse Links 58 | 59 | var properties = categoryElement.Properties as IDictionary; 60 | var links = properties["links"] as IEnumerable; 61 | 62 | string gameUri = links.First(x => x.rel == "game").uri as string; 63 | category.GameID = gameUri[(gameUri.LastIndexOf('/') + 1)..]; 64 | 65 | if (properties.ContainsKey("game")) 66 | { 67 | dynamic gameElement = properties["game"].data; 68 | var game = Game.Parse(client, gameElement) as Game; 69 | category.game = new Lazy(() => game); 70 | } 71 | else 72 | { 73 | category.game = new Lazy(() => client.Games.GetGame(category.GameID)); 74 | } 75 | 76 | if (properties.ContainsKey("variables")) 77 | { 78 | Variable parser(dynamic x) 79 | { 80 | return Variable.Parse(client, x) as Variable; 81 | } 82 | 83 | dynamic variables = client.ParseCollection(properties["variables"].data, (Func)parser); 84 | category.variables = new Lazy>(() => variables); 85 | } 86 | else 87 | { 88 | category.variables = new Lazy>(() => client.Categories.GetVariables(category.ID)); 89 | } 90 | 91 | category.Runs = client.Runs.GetRuns(categoryId: category.ID); 92 | 93 | if (category.Type == CategoryType.PerGame) 94 | { 95 | 96 | category.leaderboard = new Lazy(() => 97 | { 98 | Leaderboard leaderboard = client.Leaderboards 99 | .GetLeaderboardForFullGameCategory(category.GameID, category.ID); 100 | 101 | leaderboard.game = new Lazy(() => category.Game); 102 | leaderboard.category = new Lazy(() => category); 103 | 104 | foreach (Record record in leaderboard.Records) 105 | { 106 | record.game = leaderboard.game; 107 | record.category = leaderboard.category; 108 | } 109 | 110 | return leaderboard; 111 | }); 112 | category.worldRecord = new Lazy(() => 113 | { 114 | if (category.leaderboard.IsValueCreated) 115 | { 116 | return category.Leaderboard.Records.FirstOrDefault(); 117 | } 118 | 119 | Leaderboard leaderboard = client.Leaderboards 120 | .GetLeaderboardForFullGameCategory(category.GameID, category.ID, top: 1); 121 | 122 | leaderboard.game = new Lazy(() => category.Game); 123 | leaderboard.category = new Lazy(() => category); 124 | 125 | foreach (Record record in leaderboard.Records) 126 | { 127 | record.game = leaderboard.game; 128 | record.category = leaderboard.category; 129 | } 130 | 131 | return leaderboard.Records.FirstOrDefault(); 132 | }); 133 | } 134 | else 135 | { 136 | category.leaderboard = new Lazy(() => null); 137 | category.worldRecord = new Lazy(() => null); 138 | } 139 | 140 | return category; 141 | } 142 | 143 | public override int GetHashCode() 144 | { 145 | return (ID ?? string.Empty).GetHashCode(); 146 | } 147 | 148 | public override bool Equals(object obj) 149 | { 150 | if (obj is not Category other) 151 | { 152 | return false; 153 | } 154 | 155 | return ID == other.ID; 156 | } 157 | 158 | public override string ToString() 159 | { 160 | return Name; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/CategoryEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct CategoryEmbeds 4 | { 5 | private Embeds embeds; 6 | 7 | public bool EmbedGame { get => embeds["game"]; set => embeds["game"] = value; } 8 | public bool EmbedVariables { get => embeds["variables"]; set => embeds["variables"] = value; } 9 | 10 | /// 11 | /// Options for embedding resources in Category responses. 12 | /// 13 | /// Dictates whether a Game object is included in the response. 14 | /// Dictates whether a Collection of Variable objects is included in the response. 15 | public CategoryEmbeds( 16 | bool embedGame = false, 17 | bool embedVariables = false) 18 | { 19 | embeds = new Embeds(); 20 | EmbedGame = embedGame; 21 | EmbedVariables = embedVariables; 22 | } 23 | 24 | public override string ToString() 25 | { 26 | return embeds.ToString(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/CategoryType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum CategoryType 4 | { 5 | PerGame, PerLevel 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Categories/Players.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public class Players 4 | { 5 | public PlayersType Type { get; private set; } 6 | public int Value { get; private set; } 7 | 8 | private Players() { } 9 | 10 | public static Players Parse(SpeedrunComClient client, dynamic playersElement) 11 | { 12 | var players = new Players 13 | { 14 | Value = (int)playersElement.value, 15 | Type = playersElement.type == "exactly" ? PlayersType.Exactly : PlayersType.UpTo 16 | }; 17 | 18 | return players; 19 | } 20 | 21 | public override string ToString() 22 | { 23 | return Type.ToString() + " " + Value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/Assets.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class Assets 6 | { 7 | public ImageAsset Logo { get; private set; } 8 | public ImageAsset CoverTiny { get; private set; } 9 | public ImageAsset CoverSmall { get; private set; } 10 | public ImageAsset CoverMedium { get; private set; } 11 | public ImageAsset CoverLarge { get; private set; } 12 | public ImageAsset Icon { get; private set; } 13 | public ImageAsset TrophyFirstPlace { get; private set; } 14 | public ImageAsset TrophySecondPlace { get; private set; } 15 | public ImageAsset TrophyThirdPlace { get; private set; } 16 | public ImageAsset TrophyFourthPlace { get; private set; } 17 | public ImageAsset BackgroundImage { get; private set; } 18 | public ImageAsset ForegroundImage { get; private set; } 19 | 20 | private Assets() { } 21 | 22 | public static Assets Parse(SpeedrunComClient client, dynamic assetsElement) 23 | { 24 | var assets = new Assets(); 25 | 26 | var properties = assetsElement.Properties as IDictionary; 27 | 28 | assets.Logo = ImageAsset.Parse(client, assetsElement.logo) as ImageAsset; 29 | assets.CoverTiny = ImageAsset.Parse(client, properties["cover-tiny"]) as ImageAsset; 30 | assets.CoverSmall = ImageAsset.Parse(client, properties["cover-small"]) as ImageAsset; 31 | assets.CoverMedium = ImageAsset.Parse(client, properties["cover-medium"]) as ImageAsset; 32 | assets.CoverLarge = ImageAsset.Parse(client, properties["cover-large"]) as ImageAsset; 33 | assets.Icon = ImageAsset.Parse(client, assetsElement.icon) as ImageAsset; 34 | assets.TrophyFirstPlace = ImageAsset.Parse(client, properties["trophy-1st"]) as ImageAsset; 35 | assets.TrophySecondPlace = ImageAsset.Parse(client, properties["trophy-2nd"]) as ImageAsset; 36 | assets.TrophyThirdPlace = ImageAsset.Parse(client, properties["trophy-3rd"]) as ImageAsset; 37 | assets.TrophyFourthPlace = ImageAsset.Parse(client, properties["trophy-4th"]) as ImageAsset; 38 | assets.BackgroundImage = ImageAsset.Parse(client, assetsElement.background) as ImageAsset; 39 | assets.ForegroundImage = ImageAsset.Parse(client, assetsElement.foreground) as ImageAsset; 40 | 41 | return assets; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/ImageAsset.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class ImageAsset 6 | { 7 | public Uri Uri { get; private set; } 8 | 9 | private ImageAsset() { } 10 | 11 | public static ImageAsset Parse(SpeedrunComClient client, dynamic imageElement) 12 | { 13 | if (imageElement == null || imageElement.uri == null) 14 | { 15 | return null; 16 | } 17 | 18 | var image = new ImageAsset(); 19 | 20 | string uri = imageElement.uri as string; 21 | image.Uri = new Uri(uri); 22 | 23 | return image; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/Moderator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class Moderator 7 | { 8 | public string UserID { get; private set; } 9 | public ModeratorType Type { get; private set; } 10 | 11 | #region Links 12 | 13 | internal Lazy user; 14 | 15 | public User User => user.Value; 16 | public string Name => User.Name; 17 | 18 | #endregion 19 | 20 | private Moderator() { } 21 | 22 | public static Moderator Parse(SpeedrunComClient client, KeyValuePair moderatorElement) 23 | { 24 | var moderator = new Moderator 25 | { 26 | UserID = moderatorElement.Key, 27 | Type = (moderatorElement.Value as string) == "moderator" 28 | ? ModeratorType.Moderator 29 | : ModeratorType.SuperModerator 30 | }; 31 | 32 | moderator.user = new Lazy(() => client.Users.GetUser(moderator.UserID)); 33 | 34 | return moderator; 35 | } 36 | 37 | public override string ToString() 38 | { 39 | return Name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/ModeratorType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum ModeratorType 4 | { 5 | Moderator, 6 | SuperModerator 7 | } 8 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/Player.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class Player 7 | { 8 | public bool IsUser => string.IsNullOrEmpty(GuestName); 9 | public string UserID { get; private set; } 10 | public string GuestName { get; private set; } 11 | 12 | #region Links 13 | 14 | internal Lazy user; 15 | private Lazy guest; 16 | 17 | public User User => user.Value; 18 | public Guest Guest => guest.Value; 19 | public string Name => IsUser ? User.Name : GuestName; 20 | 21 | #endregion 22 | 23 | private Player() { } 24 | 25 | public static Player Parse(SpeedrunComClient client, dynamic playerElement) 26 | { 27 | var player = new Player(); 28 | 29 | var properties = playerElement.Properties as IDictionary; 30 | 31 | if (properties.ContainsKey("uri")) 32 | { 33 | if ((playerElement.rel as string) == "user") 34 | { 35 | player.UserID = playerElement.id as string; 36 | player.user = new Lazy(() => client.Users.GetUser(player.UserID)); 37 | player.guest = new Lazy(() => null); 38 | } 39 | else 40 | { 41 | player.GuestName = playerElement.name as string; 42 | player.guest = new Lazy(() => client.Guests.GetGuest(player.GuestName)); 43 | player.user = new Lazy(() => null); 44 | } 45 | } 46 | else 47 | { 48 | if ((playerElement.rel as string) == "user") 49 | { 50 | var user = User.Parse(client, playerElement) as User; 51 | player.user = new Lazy(() => user); 52 | player.UserID = user.ID; 53 | player.guest = new Lazy(() => null); 54 | } 55 | else 56 | { 57 | var guest = Guest.Parse(client, playerElement) as Guest; 58 | player.guest = new Lazy(() => guest); 59 | player.GuestName = guest.Name; 60 | player.user = new Lazy(() => null); 61 | } 62 | } 63 | 64 | return player; 65 | } 66 | 67 | public override int GetHashCode() 68 | { 69 | return (UserID ?? string.Empty).GetHashCode() 70 | ^ (GuestName ?? string.Empty).GetHashCode(); 71 | } 72 | 73 | public override bool Equals(object obj) 74 | { 75 | if (obj is not Player player) 76 | { 77 | return false; 78 | } 79 | 80 | return UserID == player.UserID 81 | && GuestName == player.GuestName; 82 | } 83 | 84 | public override string ToString() 85 | { 86 | return Name; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/PlayersType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum PlayersType 4 | { 5 | Exactly, UpTo 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Common/TimingMethod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public enum TimingMethod 6 | { 7 | GameTime, RealTime, RealTimeWithoutLoads 8 | } 9 | 10 | public static class TimingMethodHelpers 11 | { 12 | public static string ToAPIString(this TimingMethod timingMethod) 13 | { 14 | return timingMethod switch 15 | { 16 | TimingMethod.RealTime => "realtime", 17 | TimingMethod.RealTimeWithoutLoads => "realtime_noloads", 18 | TimingMethod.GameTime => "ingame", 19 | _ => throw new ArgumentException("timingMethod"), 20 | }; 21 | } 22 | 23 | public static TimingMethod FromString(string element) 24 | { 25 | return element switch 26 | { 27 | "realtime" => TimingMethod.RealTime, 28 | "realtime_noloads" => TimingMethod.RealTimeWithoutLoads, 29 | "ingame" => TimingMethod.GameTime, 30 | _ => throw new ArgumentException("element"), 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/ElementDescription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class ElementDescription 6 | { 7 | public string ID { get; private set; } 8 | public ElementType Type { get; private set; } 9 | 10 | internal ElementDescription(string id, ElementType type) 11 | { 12 | ID = id; 13 | Type = type; 14 | } 15 | 16 | private static ElementType parseUriType(string type) 17 | { 18 | return type switch 19 | { 20 | CategoriesClient.Name => ElementType.Category, 21 | GamesClient.Name => ElementType.Game, 22 | GuestsClient.Name => ElementType.Guest, 23 | LevelsClient.Name => ElementType.Level, 24 | NotificationsClient.Name => ElementType.Notification, 25 | PlatformsClient.Name => ElementType.Platform, 26 | RegionsClient.Name => ElementType.Region, 27 | RunsClient.Name => ElementType.Run, 28 | SeriesClient.Name => ElementType.Series, 29 | UsersClient.Name => ElementType.User, 30 | VariablesClient.Name => ElementType.Variable, 31 | _ => throw new ArgumentException("type"), 32 | }; 33 | } 34 | 35 | public static ElementDescription ParseUri(string uri) 36 | { 37 | string[] splits = uri.Split('/'); 38 | 39 | if (splits.Length < 2) 40 | { 41 | return null; 42 | } 43 | 44 | string id = splits[^1]; 45 | string uriTypeString = splits[^2]; 46 | 47 | try 48 | { 49 | ElementType uriType = parseUriType(uriTypeString); 50 | return new ElementDescription(id, uriType); 51 | } 52 | catch 53 | { 54 | return null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/ElementType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum ElementType 4 | { 5 | Category, 6 | Game, 7 | Guest, 8 | Level, 9 | Notification, 10 | Platform, 11 | Region, 12 | Run, 13 | Series, 14 | User, 15 | Variable 16 | } 17 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Embeds.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | internal struct Embeds 8 | { 9 | private Dictionary embedDictionary; 10 | 11 | public bool this[string name] 12 | { 13 | get 14 | { 15 | MakeSureInit(); 16 | 17 | if (embedDictionary.ContainsKey(name)) 18 | { 19 | return embedDictionary[name]; 20 | } 21 | else 22 | { 23 | return false; 24 | } 25 | } 26 | set 27 | { 28 | MakeSureInit(); 29 | 30 | if (embedDictionary.ContainsKey(name)) 31 | { 32 | embedDictionary[name] = value; 33 | } 34 | else 35 | { 36 | embedDictionary.Add(name, value); 37 | } 38 | } 39 | } 40 | 41 | private void MakeSureInit() 42 | { 43 | embedDictionary ??= []; 44 | } 45 | 46 | public override string ToString() 47 | { 48 | MakeSureInit(); 49 | 50 | if (!embedDictionary.Values.Any(x => x)) 51 | { 52 | return ""; 53 | } 54 | 55 | return "embed=" + 56 | embedDictionary 57 | .Where(x => x.Value) 58 | .Select(x => Uri.EscapeDataString(x.Key)) 59 | .Aggregate(","); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Games/GameEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct GameEmbeds 4 | { 5 | private Embeds embeds; 6 | 7 | public bool EmbedLevels 8 | { 9 | get => embeds["levels"]; 10 | set => embeds["levels"] = value; 11 | } 12 | public bool EmbedCategories 13 | { 14 | get => embeds["categories"]; 15 | set => embeds["categories"] = value; 16 | } 17 | public bool EmbedModerators 18 | { 19 | get => embeds["moderators"]; 20 | set => embeds["moderators"] = value; 21 | } 22 | public bool EmbedPlatforms 23 | { 24 | get => embeds["platforms"]; 25 | set => embeds["platforms"] = value; 26 | } 27 | public bool EmbedRegions 28 | { 29 | get => embeds["regions"]; 30 | set => embeds["regions"] = value; 31 | } 32 | public bool EmbedVariables 33 | { 34 | get => embeds["variables"]; 35 | set => embeds["variables"] = value; 36 | } 37 | 38 | /// 39 | /// Options for embedding resources in Game responses. 40 | /// 41 | /// Dictates whether a Collection of Level objects is included in the response. 42 | /// Dictates whether a Collection of Category objects is included in the response. 43 | /// Dictates whether a Collection of User objects containing each moderator is included in the response. 44 | /// Dictates whether a Collection of Platform objects is included in the response. 45 | /// Dictates whether a Collection of Region objects is included in the response. 46 | /// Dictates whether a Collection of Variable objects is included in the response. 47 | public GameEmbeds( 48 | bool embedLevels = false, 49 | bool embedCategories = false, 50 | bool embedModerators = false, 51 | bool embedPlatforms = false, 52 | bool embedRegions = false, 53 | bool embedVariables = false) 54 | { 55 | embeds = new Embeds(); 56 | EmbedLevels = embedLevels; 57 | EmbedCategories = embedCategories; 58 | EmbedModerators = embedModerators; 59 | EmbedPlatforms = embedPlatforms; 60 | EmbedRegions = embedRegions; 61 | EmbedVariables = embedVariables; 62 | } 63 | 64 | public override string ToString() 65 | { 66 | return embeds.ToString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Games/GameHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// An optimized class for simple data for games. 7 | /// 8 | public class GameHeader : IElementWithID 9 | { 10 | public string ID { get; private set; } 11 | public string Name { get; private set; } 12 | public string JapaneseName { get; private set; } 13 | public string TwitchName { get; private set; } 14 | public string Abbreviation { get; private set; } 15 | public Uri WebLink { get; private set; } 16 | 17 | private GameHeader() { } 18 | 19 | public static GameHeader Parse(SpeedrunComClient client, dynamic gameHeaderElement) 20 | { 21 | var gameHeader = new GameHeader 22 | { 23 | ID = gameHeaderElement.id as string, 24 | Name = gameHeaderElement.names.international as string, 25 | JapaneseName = gameHeaderElement.names.japanese as string, 26 | TwitchName = gameHeaderElement.names.twitch as string, 27 | WebLink = new Uri(gameHeaderElement.weblink as string), 28 | Abbreviation = gameHeaderElement.abbreviation as string 29 | }; 30 | 31 | return gameHeader; 32 | } 33 | 34 | public override int GetHashCode() 35 | { 36 | return (ID ?? string.Empty).GetHashCode(); 37 | } 38 | 39 | public override bool Equals(object obj) 40 | { 41 | if (obj is not GameHeader other) 42 | { 43 | return false; 44 | } 45 | 46 | return ID == other.ID; 47 | } 48 | 49 | public override string ToString() 50 | { 51 | return Name; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Games/GamesOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Games in responses. 7 | /// 8 | public enum GamesOrdering : int 9 | { 10 | Similarity = 0, 11 | SimilarityDescending, 12 | Name, 13 | NameDescending, 14 | JapaneseName, 15 | JapaneseNameDescending, 16 | Abbreviation, 17 | AbbreviationDescending, 18 | YearOfRelease, 19 | YearOfReleaseDescending, 20 | CreationDate, 21 | CreationDateDescending 22 | } 23 | 24 | internal static class GamesOrderingHelpers 25 | { 26 | internal static IEnumerable ToParameters(this GamesOrdering ordering) 27 | { 28 | bool isDescending = ((int)ordering & 1) == 1; 29 | if (isDescending) 30 | { 31 | ordering = (GamesOrdering)((int)ordering - 1); 32 | } 33 | 34 | string str = ""; 35 | 36 | switch (ordering) 37 | { 38 | case GamesOrdering.Name: 39 | str = "name.int"; break; 40 | case GamesOrdering.JapaneseName: 41 | str = "name.jap"; break; 42 | case GamesOrdering.Abbreviation: 43 | str = "abbreviation"; break; 44 | case GamesOrdering.YearOfRelease: 45 | str = "released"; break; 46 | case GamesOrdering.CreationDate: 47 | str = "created"; break; 48 | } 49 | 50 | var list = new List(); 51 | 52 | if (!string.IsNullOrEmpty(str)) 53 | { 54 | list.Add(string.Format("orderby={0}", str)); 55 | } 56 | 57 | if (isDescending) 58 | { 59 | list.Add("direction=desc"); 60 | } 61 | 62 | return list; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Games/Ruleset.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace SpeedrunComSharp; 7 | 8 | public class Ruleset 9 | { 10 | public bool ShowMilliseconds { get; private set; } 11 | public bool RequiresVerification { get; private set; } 12 | public bool RequiresVideo { get; private set; } 13 | public ReadOnlyCollection TimingMethods { get; private set; } 14 | public TimingMethod DefaultTimingMethod { get; private set; } 15 | public bool EmulatorsAllowed { get; private set; } 16 | 17 | private Ruleset() { } 18 | 19 | public static Ruleset Parse(SpeedrunComClient client, dynamic rulesetElement) 20 | { 21 | var ruleset = new Ruleset(); 22 | 23 | var properties = rulesetElement.Properties as IDictionary; 24 | 25 | ruleset.ShowMilliseconds = properties["show-milliseconds"]; 26 | ruleset.RequiresVerification = properties["require-verification"]; 27 | ruleset.RequiresVideo = properties["require-video"]; 28 | 29 | static TimingMethod timingMethodParser(dynamic x) 30 | { 31 | return TimingMethodHelpers.FromString(x as string); 32 | } 33 | 34 | ruleset.TimingMethods = client.ParseCollection(properties["run-times"], (Func)timingMethodParser); 35 | ruleset.DefaultTimingMethod = TimingMethodHelpers.FromString(properties["default-time"]); 36 | 37 | ruleset.EmulatorsAllowed = properties["emulators-allowed"]; 38 | 39 | return ruleset; 40 | } 41 | 42 | public override string ToString() 43 | { 44 | var list = new List(); 45 | if (ShowMilliseconds) 46 | { 47 | list.Add("Show Milliseconds"); 48 | } 49 | 50 | if (RequiresVerification) 51 | { 52 | list.Add("Requires Verification"); 53 | } 54 | 55 | if (RequiresVideo) 56 | { 57 | list.Add("Requires Video"); 58 | } 59 | 60 | if (EmulatorsAllowed) 61 | { 62 | list.Add("Emulators Allowed"); 63 | } 64 | 65 | if (!list.Any()) 66 | { 67 | list.Add("No Rules"); 68 | } 69 | 70 | return list.Aggregate(", "); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Guests/Guest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class Guest 6 | { 7 | public string Name { get; private set; } 8 | 9 | #region Links 10 | 11 | public IEnumerable Runs { get; private set; } 12 | 13 | #endregion 14 | 15 | private Guest() { } 16 | 17 | public static Guest Parse(SpeedrunComClient client, dynamic guestElement) 18 | { 19 | var guest = new Guest 20 | { 21 | Name = guestElement.name 22 | }; 23 | guest.Runs = client.Runs.GetRuns(guestName: guest.Name); 24 | 25 | return guest; 26 | } 27 | 28 | public override string ToString() 29 | { 30 | return Name; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Guests/GuestsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class GuestsClient 6 | { 7 | public const string Name = "guests"; 8 | 9 | private readonly SpeedrunComClient baseClient; 10 | 11 | public GuestsClient(SpeedrunComClient baseClient) 12 | { 13 | this.baseClient = baseClient; 14 | } 15 | 16 | public static Uri GetGuestsUri(string subUri) 17 | { 18 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 19 | } 20 | 21 | /// 22 | /// Fetch a Guest object identified by its URI. 23 | /// 24 | /// The site URI for the guest. 25 | /// 26 | public Guest GetGuestFromSiteUri(string siteUri) 27 | { 28 | string id = GetGuestIDFromSiteUri(siteUri); 29 | 30 | if (string.IsNullOrEmpty(id)) 31 | { 32 | return null; 33 | } 34 | 35 | return GetGuest(id); 36 | } 37 | 38 | /// 39 | /// Fetch a Guest ID identified by its URI. 40 | /// 41 | /// The site URI for the guest. 42 | /// 43 | public string GetGuestIDFromSiteUri(string siteUri) 44 | { 45 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 46 | 47 | if (elementDescription == null 48 | || elementDescription.Type != ElementType.Guest) 49 | { 50 | return null; 51 | } 52 | 53 | return elementDescription.ID; 54 | } 55 | 56 | /// 57 | /// Fetch a Guest object identified by its name. 58 | /// 59 | /// The name of the guest. 60 | /// 61 | public Guest GetGuest(string guestName) 62 | { 63 | Uri uri = GetGuestsUri(string.Format("/{0}", Uri.EscapeDataString(guestName))); 64 | dynamic result = baseClient.DoRequest(uri); 65 | 66 | return Guest.Parse(baseClient, result.data); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/HttpWebLink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | internal class HttpWebLink 8 | { 9 | public string Uri { get; private set; } 10 | public string Relation { get; private set; } 11 | public string Anchor { get; private set; } 12 | public string RelationTypes { get; private set; } 13 | public string Language { get; private set; } 14 | public string Media { get; private set; } 15 | public string Title { get; private set; } 16 | public string Titles { get; private set; } 17 | public string Type { get; private set; } 18 | 19 | private HttpWebLink() { } 20 | 21 | public static ReadOnlyCollection ParseLinks(string linksString) 22 | { 23 | return (linksString ?? string.Empty) 24 | .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) 25 | .Select(x => ParseLink(x.Trim(' '))) 26 | .ToList() 27 | .AsReadOnly(); 28 | } 29 | 30 | public static HttpWebLink ParseLink(string linkString) 31 | { 32 | var link = new HttpWebLink(); 33 | 34 | int leftAngledParenthesis = linkString.IndexOf('<'); 35 | int rightAngledParenthesis = linkString.IndexOf('>'); 36 | 37 | if (leftAngledParenthesis >= 0 && rightAngledParenthesis >= 0) 38 | { 39 | link.Uri = linkString.Substring(leftAngledParenthesis + 1, rightAngledParenthesis - leftAngledParenthesis - 1); 40 | } 41 | 42 | linkString = linkString[(rightAngledParenthesis + 1)..]; 43 | string[] parameters = linkString.Split(new[] { "; " }, StringSplitOptions.RemoveEmptyEntries); 44 | 45 | foreach (string parameter in parameters) 46 | { 47 | string[] splits = parameter.Split(['='], 2); 48 | if (splits.Length == 2) 49 | { 50 | string parameterType = splits[0]; 51 | string parameterValue = splits[1].Trim('"'); 52 | 53 | switch (parameterType) 54 | { 55 | case "rel": 56 | link.Relation = parameterValue; 57 | break; 58 | case "anchor": 59 | link.Anchor = parameterValue; 60 | break; 61 | case "rev": 62 | link.RelationTypes = parameterValue; 63 | break; 64 | case "hreflang": 65 | link.Language = parameterValue; 66 | break; 67 | case "media": 68 | link.Media = parameterValue; 69 | break; 70 | case "title": 71 | link.Title = parameterValue; 72 | break; 73 | case "title*": 74 | link.Titles = parameterValue; 75 | break; 76 | case "type": 77 | link.Type = parameterValue; 78 | break; 79 | } 80 | } 81 | } 82 | 83 | return link; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/IElementWithID.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public interface IElementWithID 4 | { 5 | string ID { get; } 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/JSON.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Dynamic; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Text; 11 | using System.Text.RegularExpressions; 12 | using System.Web; 13 | using System.Web.Script.Serialization; 14 | 15 | namespace SpeedrunComSharp; 16 | 17 | internal static class JSON 18 | { 19 | public static dynamic FromResponse(WebResponse response) 20 | { 21 | using Stream stream = response.GetResponseStream(); 22 | return FromStream(stream); 23 | } 24 | 25 | public static dynamic FromStream(Stream stream) 26 | { 27 | var reader = new StreamReader(stream); 28 | string json = ""; 29 | try 30 | { 31 | json = reader.ReadToEnd(); 32 | } 33 | catch { } 34 | 35 | return FromString(json); 36 | } 37 | 38 | public static dynamic FromString(string value) 39 | { 40 | var serializer = new JavaScriptSerializer() 41 | { 42 | MaxJsonLength = int.MaxValue 43 | }; 44 | serializer.RegisterConverters(new[] { new DynamicJsonConverter() }); 45 | 46 | return serializer.Deserialize(value); 47 | } 48 | 49 | public static dynamic FromUri(Uri uri, string userAgent, string accessToken, TimeSpan timeout) 50 | { 51 | var request = (HttpWebRequest)WebRequest.Create(uri); 52 | request.Timeout = (int)timeout.TotalMilliseconds; 53 | request.UserAgent = userAgent; 54 | if (!string.IsNullOrEmpty(accessToken)) 55 | { 56 | request.Headers.Add("X-API-Key", accessToken.ToString()); 57 | } 58 | 59 | WebResponse response = request.GetResponse(); 60 | return FromResponse(response); 61 | } 62 | 63 | public static string Escape(string value) 64 | { 65 | return HttpUtility.JavaScriptStringEncode(value); 66 | } 67 | 68 | public static dynamic FromUriPost(Uri uri, string userAgent, string accessToken, TimeSpan timeout, string postBody) 69 | { 70 | var request = (HttpWebRequest)WebRequest.Create(uri); 71 | request.Timeout = (int)timeout.TotalMilliseconds; 72 | request.Method = "POST"; 73 | request.UserAgent = userAgent; 74 | if (!string.IsNullOrEmpty(accessToken)) 75 | { 76 | request.Headers.Add("X-API-Key", accessToken.ToString()); 77 | } 78 | 79 | request.ContentType = "application/json"; 80 | 81 | using (var writer = new StreamWriter(request.GetRequestStream())) 82 | { 83 | writer.Write(postBody); 84 | } 85 | 86 | WebResponse response = request.GetResponse(); 87 | 88 | return FromResponse(response); 89 | } 90 | } 91 | 92 | public sealed class DynamicJsonConverter : JavaScriptConverter 93 | { 94 | public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) 95 | { 96 | if (dictionary == null) 97 | { 98 | throw new ArgumentNullException("dictionary"); 99 | } 100 | 101 | return type == typeof(object) ? new DynamicJsonObject(dictionary) : null; 102 | } 103 | 104 | public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) 105 | { 106 | throw new NotImplementedException(); 107 | } 108 | 109 | public override IEnumerable SupportedTypes => new ReadOnlyCollection(new List(new[] { typeof(object) })); 110 | } 111 | 112 | public sealed class DynamicJsonObject : DynamicObject 113 | { 114 | private readonly IDictionary _dictionary; 115 | 116 | //public IDictionary Properties { get { return _dictionary; } } 117 | 118 | public DynamicJsonObject() 119 | : this(new Dictionary()) 120 | { } 121 | 122 | public DynamicJsonObject(IDictionary dictionary) 123 | { 124 | if (dictionary == null) 125 | { 126 | throw new ArgumentNullException("dictionary"); 127 | } 128 | 129 | _dictionary = dictionary; 130 | } 131 | 132 | public override string ToString() 133 | { 134 | var sb = new StringBuilder("{\r\n"); 135 | ToString(sb); 136 | return sb.ToString(); 137 | } 138 | 139 | private void ToString(StringBuilder sb, int depth = 1) 140 | { 141 | bool firstInDictionary = true; 142 | foreach (KeyValuePair pair in _dictionary) 143 | { 144 | if (!firstInDictionary) 145 | { 146 | sb.Append(",\r\n"); 147 | } 148 | 149 | sb.Append('\t', depth); 150 | firstInDictionary = false; 151 | object value = pair.Value; 152 | string name = pair.Key; 153 | if (value is IEnumerable array) 154 | { 155 | sb.Append("\"" + HttpUtility.JavaScriptStringEncode(name) + "\": [\r\n"); 156 | bool firstInArray = true; 157 | foreach (object arrayValue in array) 158 | { 159 | if (!firstInArray) 160 | { 161 | sb.Append(",\r\n"); 162 | } 163 | 164 | sb.Append('\t', depth + 1); 165 | firstInArray = false; 166 | if (arrayValue is IDictionary dict) 167 | { 168 | new DynamicJsonObject(dict).ToString(sb, depth + 2); 169 | } 170 | else if (arrayValue is DynamicJsonObject obj) 171 | { 172 | sb.Append("{\r\n"); 173 | obj.ToString(sb, depth + 2); 174 | } 175 | else if (arrayValue is string str) 176 | { 177 | sb.AppendFormat("\"{0}\"", HttpUtility.JavaScriptStringEncode(str)); 178 | } 179 | else if (arrayValue is bool b) 180 | { 181 | sb.AppendFormat("{0}", HttpUtility.JavaScriptStringEncode(b.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); 182 | } 183 | else if (arrayValue is int i) 184 | { 185 | sb.AppendFormat("{0}", HttpUtility.JavaScriptStringEncode(i.ToString(CultureInfo.InvariantCulture))); 186 | } 187 | else if (arrayValue is double d) 188 | { 189 | sb.AppendFormat("{0}", HttpUtility.JavaScriptStringEncode(d.ToString(CultureInfo.InvariantCulture))); 190 | } 191 | else if (arrayValue is decimal m) 192 | { 193 | sb.AppendFormat("{0}", HttpUtility.JavaScriptStringEncode(m.ToString(CultureInfo.InvariantCulture))); 194 | } 195 | else 196 | { 197 | sb.AppendFormat("\"{0}\"", HttpUtility.JavaScriptStringEncode((arrayValue ?? "").ToString())); 198 | } 199 | } 200 | 201 | sb.Append("\r\n"); 202 | sb.Append('\t', depth); 203 | sb.Append("]"); 204 | } 205 | else if (value is IDictionary dict) 206 | { 207 | sb.Append("\"" + HttpUtility.JavaScriptStringEncode(name) + "\": {\r\n"); 208 | new DynamicJsonObject(dict).ToString(sb, depth + 1); 209 | } 210 | else if (value is DynamicJsonObject obj) 211 | { 212 | sb.Append("\"" + HttpUtility.JavaScriptStringEncode(name) + "\": {\r\n"); 213 | obj.ToString(sb, depth + 1); 214 | } 215 | else if (value is string str) 216 | { 217 | sb.AppendFormat("\"{0}\": \"{1}\"", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode(str)); 218 | } 219 | else if (value is bool b) 220 | { 221 | sb.AppendFormat("\"{0}\": {1}", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode(b.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); 222 | } 223 | else if (value is int i) 224 | { 225 | sb.AppendFormat("\"{0}\": {1}", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode(i.ToString(CultureInfo.InvariantCulture))); 226 | } 227 | else if (value is double d) 228 | { 229 | sb.AppendFormat("\"{0}\": {1}", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode(d.ToString(CultureInfo.InvariantCulture))); 230 | } 231 | else if (value is decimal m) 232 | { 233 | sb.AppendFormat("\"{0}\": {1}", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode(m.ToString(CultureInfo.InvariantCulture))); 234 | } 235 | else 236 | { 237 | sb.AppendFormat("\"{0}\": \"{1}\"", HttpUtility.JavaScriptStringEncode(name), HttpUtility.JavaScriptStringEncode((value ?? "").ToString())); 238 | } 239 | } 240 | 241 | sb.Append("\r\n"); 242 | sb.Append('\t', depth - 1); 243 | sb.Append("}"); 244 | } 245 | 246 | public override bool TrySetMember(SetMemberBinder binder, object value) 247 | { 248 | if (_dictionary.ContainsKey(binder.Name)) 249 | { 250 | _dictionary[binder.Name] = value; 251 | return true; 252 | } 253 | else 254 | { 255 | _dictionary.Add(binder.Name, value); 256 | return true; 257 | } 258 | } 259 | 260 | public override bool TryGetMember(GetMemberBinder binder, out object result) 261 | { 262 | if (binder.Name == "Properties") 263 | { 264 | result = _dictionary 265 | .Select(x => new KeyValuePair(x.Key, WrapResultObject(x.Value))) 266 | .ToDictionary(x => x.Key, x => x.Value); 267 | return true; 268 | } 269 | 270 | if (!_dictionary.TryGetValue(binder.Name, out result)) 271 | { 272 | // return null to avoid exception. caller can check for null this way... 273 | result = null; 274 | return true; 275 | } 276 | 277 | result = WrapResultObject(result); 278 | 279 | if (result is string) 280 | { 281 | result = JavaScriptStringDecode(result as string); 282 | } 283 | 284 | return true; 285 | } 286 | 287 | public static string JavaScriptStringDecode(string source) 288 | { 289 | // Replace some chars. 290 | string decoded = source.Replace(@"\'", "'") 291 | .Replace(@"\""", @"""") 292 | .Replace(@"\/", "/") 293 | .Replace(@"\t", "\t") 294 | .Replace(@"\n", "\n"); 295 | 296 | // Replace unicode escaped text. 297 | var rx = new Regex(@"\\[uU]([0-9A-F]{4})"); 298 | 299 | decoded = rx.Replace(decoded, match => ((char)int.Parse(match.Value[2..], NumberStyles.HexNumber)) 300 | .ToString(CultureInfo.InvariantCulture)); 301 | 302 | return decoded; 303 | } 304 | 305 | public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) 306 | { 307 | if (indexes.Length == 1 && indexes[0] != null) 308 | { 309 | if (!_dictionary.TryGetValue(indexes[0].ToString(), out result)) 310 | { 311 | // return null to avoid exception. caller can check for null this way... 312 | result = null; 313 | return true; 314 | } 315 | 316 | result = WrapResultObject(result); 317 | return true; 318 | } 319 | 320 | return base.TryGetIndex(binder, indexes, out result); 321 | } 322 | 323 | private static object WrapResultObject(object result) 324 | { 325 | if (result is IDictionary dictionary) 326 | { 327 | return new DynamicJsonObject(dictionary); 328 | } 329 | 330 | if (result is ArrayList arrayList && arrayList.Count > 0) 331 | { 332 | return arrayList[0] is IDictionary 333 | ? new List(arrayList.Cast>().Select(x => new DynamicJsonObject(x))) 334 | : new List(arrayList.Cast()); 335 | } 336 | 337 | return result; 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/EmulatorsFilter.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | /// 4 | /// Filters for whether emulators are included in Leaderboards. 5 | /// 6 | public enum EmulatorsFilter 7 | { 8 | NotSet, 9 | OnlyEmulators, 10 | NoEmulators 11 | } 12 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/Leaderboard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | 6 | namespace SpeedrunComSharp; 7 | 8 | public class Leaderboard 9 | { 10 | public Uri WebLink { get; private set; } 11 | public EmulatorsFilter EmulatorFilter { get; private set; } 12 | public bool AreRunsWithoutVideoFilteredOut { get; private set; } 13 | public TimingMethod? OrderedBy { get; private set; } 14 | public ReadOnlyCollection VariableFilters { get; private set; } 15 | 16 | public ReadOnlyCollection Records { get; private set; } 17 | 18 | #region Embeds 19 | 20 | private Lazy> players; 21 | private Lazy> usedRegions; 22 | private Lazy> usedPlatforms; 23 | private Lazy> applicableVariables; 24 | 25 | public ReadOnlyCollection Players => players.Value; 26 | public ReadOnlyCollection UsedRegions => usedRegions.Value; 27 | public ReadOnlyCollection UsedPlatforms => usedPlatforms.Value; 28 | public ReadOnlyCollection ApplicableVariables => applicableVariables.Value; 29 | 30 | #endregion 31 | 32 | #region Links 33 | 34 | internal Lazy game; 35 | internal Lazy category; 36 | private Lazy level; 37 | private Lazy platformFilter; 38 | private Lazy regionFilter; 39 | 40 | public string GameID { get; private set; } 41 | public Game Game => game.Value; 42 | public string CategoryID { get; private set; } 43 | public Category Category => category.Value; 44 | public string LevelID { get; private set; } 45 | public Level Level => level.Value; 46 | public string PlatformIDOfFilter { get; private set; } 47 | public Platform PlatformFilter => platformFilter.Value; 48 | public string RegionIDOfFilter { get; private set; } 49 | public Region RegionFilter => regionFilter.Value; 50 | 51 | #endregion 52 | 53 | private Leaderboard() { } 54 | 55 | public static Leaderboard Parse(SpeedrunComClient client, dynamic leaderboardElement) 56 | { 57 | var leaderboard = new Leaderboard(); 58 | 59 | var properties = leaderboardElement.Properties as IDictionary; 60 | 61 | //Parse Attributes 62 | 63 | leaderboard.WebLink = new Uri(leaderboardElement.weblink as string); 64 | 65 | string emulators = leaderboardElement.emulators as string; 66 | if (emulators == "true") 67 | { 68 | leaderboard.EmulatorFilter = EmulatorsFilter.OnlyEmulators; 69 | } 70 | else if (emulators == "false") 71 | { 72 | leaderboard.EmulatorFilter = EmulatorsFilter.NoEmulators; 73 | } 74 | else 75 | { 76 | leaderboard.EmulatorFilter = EmulatorsFilter.NotSet; 77 | } 78 | 79 | leaderboard.AreRunsWithoutVideoFilteredOut = properties["video-only"]; 80 | 81 | //TODO Not actually optional 82 | if (leaderboardElement.timing != null) 83 | { 84 | leaderboard.OrderedBy = TimingMethodHelpers.FromString(leaderboardElement.timing as string); 85 | } 86 | 87 | if (leaderboardElement.values is DynamicJsonObject) 88 | { 89 | var valueProperties = leaderboardElement.values.Properties as IDictionary; 90 | leaderboard.VariableFilters = valueProperties.Select(x => VariableValue.ParseValueDescriptor(client, x)).ToList().AsReadOnly(); 91 | } 92 | else 93 | { 94 | leaderboard.VariableFilters = new List().AsReadOnly(); 95 | } 96 | 97 | Record recordParser(dynamic x) 98 | { 99 | return Record.Parse(client, x) as Record; 100 | } 101 | 102 | leaderboard.Records = client.ParseCollection(leaderboardElement.runs, (Func)recordParser); 103 | 104 | //Parse Links 105 | 106 | if (properties["game"] is string) 107 | { 108 | leaderboard.GameID = leaderboardElement.game as string; 109 | leaderboard.game = new Lazy(() => client.Games.GetGame(leaderboard.GameID)); 110 | } 111 | else 112 | { 113 | var game = Game.Parse(client, properties["game"].data) as Game; 114 | leaderboard.game = new Lazy(() => game); 115 | leaderboard.GameID = game.ID; 116 | } 117 | 118 | if (properties["category"] is string) 119 | { 120 | leaderboard.CategoryID = leaderboardElement.category as string; 121 | leaderboard.category = new Lazy(() => client.Categories.GetCategory(leaderboard.CategoryID)); 122 | } 123 | else 124 | { 125 | var category = Category.Parse(client, properties["category"].data) as Category; 126 | leaderboard.category = new Lazy(() => category); 127 | if (category != null) 128 | { 129 | leaderboard.CategoryID = category.ID; 130 | } 131 | } 132 | 133 | if (properties["level"] == null) 134 | { 135 | leaderboard.level = new Lazy(() => null); 136 | } 137 | else if (properties["level"] is string) 138 | { 139 | leaderboard.LevelID = leaderboardElement.level as string; 140 | leaderboard.level = new Lazy(() => client.Levels.GetLevel(leaderboard.LevelID)); 141 | } 142 | else 143 | { 144 | var level = Level.Parse(client, properties["level"].data) as Level; 145 | leaderboard.level = new Lazy(() => level); 146 | if (level != null) 147 | { 148 | leaderboard.LevelID = level.ID; 149 | } 150 | } 151 | 152 | if (properties["platform"] == null) 153 | { 154 | leaderboard.platformFilter = new Lazy(() => null); 155 | } 156 | else if (properties["platform"] is string) 157 | { 158 | leaderboard.PlatformIDOfFilter = properties["platform"] as string; 159 | leaderboard.platformFilter = new Lazy(() => client.Platforms.GetPlatform(leaderboard.PlatformIDOfFilter)); 160 | } 161 | else 162 | { 163 | var platform = Platform.Parse(client, properties["platform"].data) as Platform; 164 | leaderboard.platformFilter = new Lazy(() => platform); 165 | if (platform != null) 166 | { 167 | leaderboard.PlatformIDOfFilter = platform.ID; 168 | } 169 | } 170 | 171 | if (properties["region"] == null) 172 | { 173 | leaderboard.regionFilter = new Lazy(() => null); 174 | } 175 | else if (properties["region"] is string) 176 | { 177 | leaderboard.RegionIDOfFilter = properties["region"] as string; 178 | leaderboard.regionFilter = new Lazy(() => client.Regions.GetRegion(leaderboard.RegionIDOfFilter)); 179 | } 180 | else 181 | { 182 | var region = Region.Parse(client, properties["region"].data) as Region; 183 | leaderboard.regionFilter = new Lazy(() => region); 184 | if (region != null) 185 | { 186 | leaderboard.RegionIDOfFilter = region.ID; 187 | } 188 | } 189 | 190 | //Parse Embeds 191 | 192 | if (properties.ContainsKey("players")) 193 | { 194 | Player playerParser(dynamic x) 195 | { 196 | return Player.Parse(client, x) as Player; 197 | } 198 | 199 | var players = client.ParseCollection(leaderboardElement.players.data, (Func)playerParser) as ReadOnlyCollection; 200 | 201 | foreach (Record record in leaderboard.Records) 202 | { 203 | record.Players = record.Players.Select(x => players.FirstOrDefault(y => x.Equals(y))).ToList().AsReadOnly(); 204 | } 205 | 206 | leaderboard.players = new Lazy>(() => players); 207 | } 208 | else 209 | { 210 | leaderboard.players = new Lazy>(() => leaderboard.Records.SelectMany(x => x.Players).ToList().Distinct().ToList().AsReadOnly()); 211 | } 212 | 213 | if (properties.ContainsKey("regions")) 214 | { 215 | Region regionParser(dynamic x) 216 | { 217 | return Region.Parse(client, x) as Region; 218 | } 219 | 220 | var regions = client.ParseCollection(leaderboardElement.regions.data, (Func)regionParser) as ReadOnlyCollection; 221 | 222 | foreach (Record record in leaderboard.Records) 223 | { 224 | record.System.region = new Lazy(() => regions.FirstOrDefault(x => x.ID == record.System.RegionID)); 225 | } 226 | 227 | leaderboard.usedRegions = new Lazy>(() => regions); 228 | } 229 | else 230 | { 231 | leaderboard.usedRegions = new Lazy>(() => leaderboard.Records.Select(x => x.Region).Distinct().Where(x => x != null).ToList().AsReadOnly()); 232 | } 233 | 234 | if (properties.ContainsKey("platforms")) 235 | { 236 | Platform platformParser(dynamic x) 237 | { 238 | return Platform.Parse(client, x) as Platform; 239 | } 240 | 241 | var platforms = client.ParseCollection(leaderboardElement.platforms.data, (Func)platformParser) as ReadOnlyCollection; 242 | 243 | foreach (Record record in leaderboard.Records) 244 | { 245 | record.System.platform = new Lazy(() => platforms.FirstOrDefault(x => x.ID == record.System.PlatformID)); 246 | } 247 | 248 | leaderboard.usedPlatforms = new Lazy>(() => platforms); 249 | } 250 | else 251 | { 252 | leaderboard.usedPlatforms = new Lazy>(() => leaderboard.Records.Select(x => x.Platform).Distinct().Where(x => x != null).ToList().AsReadOnly()); 253 | } 254 | 255 | void patchVariablesOfRecords(ReadOnlyCollection variables) 256 | { 257 | foreach (Record record in leaderboard.Records) 258 | { 259 | foreach (VariableValue value in record.VariableValues) 260 | { 261 | value.variable = new Lazy(() => variables.FirstOrDefault(x => x.ID == value.VariableID)); 262 | } 263 | } 264 | } 265 | 266 | if (properties.ContainsKey("variables")) 267 | { 268 | Variable variableParser(dynamic x) 269 | { 270 | return Variable.Parse(client, x) as Variable; 271 | } 272 | 273 | var variables = client.ParseCollection(leaderboardElement.variables.data, (Func)variableParser) as ReadOnlyCollection; 274 | 275 | patchVariablesOfRecords(variables); 276 | 277 | leaderboard.applicableVariables = new Lazy>(() => variables); 278 | } 279 | else if (string.IsNullOrEmpty(leaderboard.LevelID)) 280 | { 281 | leaderboard.applicableVariables = new Lazy>(() => 282 | { 283 | ReadOnlyCollection variables = leaderboard.Category.Variables; 284 | 285 | patchVariablesOfRecords(variables); 286 | 287 | return variables; 288 | }); 289 | } 290 | else 291 | { 292 | leaderboard.applicableVariables = new Lazy>(() => 293 | { 294 | ReadOnlyCollection variables = leaderboard.Category.Variables.Concat(leaderboard.Level.Variables).ToList().Distinct().ToList().AsReadOnly(); 295 | 296 | patchVariablesOfRecords(variables); 297 | 298 | return variables; 299 | }); 300 | } 301 | 302 | return leaderboard; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/LeaderboardEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct LeaderboardEmbeds 4 | { 5 | private Embeds embeds; 6 | private readonly bool isConstructed; 7 | 8 | public bool EmbedGame 9 | { 10 | get => embeds["game"]; 11 | set => embeds["game"] = value; 12 | } 13 | 14 | public bool EmbedCategory 15 | { 16 | get => embeds["category"]; 17 | set => embeds["category"] = value; 18 | } 19 | 20 | public bool EmbedLevel 21 | { 22 | get => embeds["level"]; 23 | set => embeds["level"] = value; 24 | } 25 | 26 | public bool EmbedPlayers 27 | { 28 | get => embeds["players"]; 29 | set => embeds["players"] = value; 30 | } 31 | 32 | public bool EmbedRegions 33 | { 34 | get => embeds["regions"]; 35 | set => embeds["regions"] = value; 36 | } 37 | 38 | public bool EmbedPlatforms 39 | { 40 | get => embeds["platforms"]; 41 | set => embeds["platforms"] = value; 42 | } 43 | 44 | public bool EmbedVariables 45 | { 46 | get => embeds["variables"]; 47 | set => embeds["variables"] = value; 48 | } 49 | 50 | /// 51 | /// Options for embedding resources in Leaderboard responses. 52 | /// 53 | /// Dictates whether a Game object is included in the response. 54 | /// Dictates whether a Category object is included in the response. 55 | /// Dictates whether a Level object is included in the response. 56 | /// Dictates whether a Collection of Player objects is included in the response. 57 | /// Dictates whether a Collection of Region objects is included in the response. 58 | /// Dictates whether a Collection of Platform objects is included in the response. 59 | /// Dictates whether a Collection of Variable objects is included in the response. 60 | public LeaderboardEmbeds( 61 | bool embedGame = false, 62 | bool embedCategory = false, 63 | bool embedLevel = false, 64 | bool embedPlayers = true, 65 | bool embedRegions = false, 66 | bool embedPlatforms = false, 67 | bool embedVariables = false) 68 | { 69 | isConstructed = true; 70 | 71 | embeds = new Embeds(); 72 | EmbedGame = embedGame; 73 | EmbedCategory = embedCategory; 74 | EmbedLevel = embedLevel; 75 | EmbedPlayers = embedPlayers; 76 | EmbedRegions = embedRegions; 77 | EmbedPlatforms = embedPlatforms; 78 | EmbedVariables = embedVariables; 79 | } 80 | 81 | public override string ToString() 82 | { 83 | if (!isConstructed) 84 | { 85 | EmbedPlayers = true; 86 | } 87 | 88 | return embeds.ToString(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/LeaderboardScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public enum LeaderboardScope 6 | { 7 | All, FullGame, Levels 8 | } 9 | 10 | public static class LeaderboardScopeHelpers 11 | { 12 | public static string ToParameter(this LeaderboardScope scope) 13 | { 14 | return scope switch 15 | { 16 | LeaderboardScope.All => "all", 17 | LeaderboardScope.FullGame => "full-game", 18 | LeaderboardScope.Levels => "levels", 19 | _ => throw new ArgumentException("scope"), 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/LeaderboardsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class LeaderboardsClient 8 | { 9 | public const string Name = "leaderboards"; 10 | 11 | private readonly SpeedrunComClient baseClient; 12 | 13 | public LeaderboardsClient(SpeedrunComClient baseClient) 14 | { 15 | this.baseClient = baseClient; 16 | } 17 | 18 | public static Uri GetLeaderboardsUri(string subUri) 19 | { 20 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 21 | } 22 | 23 | private Leaderboard getLeaderboard( 24 | string uri, int? top = null, 25 | string platformId = null, string regionId = null, 26 | EmulatorsFilter emulatorsFilter = EmulatorsFilter.NotSet, bool filterOutRunsWithoutVideo = false, 27 | TimingMethod? orderBy = null, DateTime? filterOutRunsAfter = null, 28 | IEnumerable variableFilters = null, 29 | LeaderboardEmbeds embeds = default) 30 | { 31 | var parameters = new List() { embeds.ToString() }; 32 | 33 | if (top.HasValue) 34 | { 35 | parameters.Add(string.Format("top={0}", top.Value)); 36 | } 37 | 38 | if (!string.IsNullOrEmpty(platformId)) 39 | { 40 | parameters.Add(string.Format("platform={0}", Uri.EscapeDataString(platformId))); 41 | } 42 | 43 | if (!string.IsNullOrEmpty(regionId)) 44 | { 45 | parameters.Add(string.Format("region={0}", Uri.EscapeDataString(regionId))); 46 | } 47 | 48 | if (emulatorsFilter != EmulatorsFilter.NotSet) 49 | { 50 | parameters.Add(string.Format("emulators={0}", 51 | emulatorsFilter == EmulatorsFilter.OnlyEmulators ? "true" : "false")); 52 | } 53 | 54 | if (filterOutRunsWithoutVideo) 55 | { 56 | parameters.Add("video-only=true"); 57 | } 58 | 59 | if (orderBy.HasValue) 60 | { 61 | string timing = orderBy.Value.ToAPIString(); 62 | parameters.Add(string.Format("timing={0}", timing)); 63 | } 64 | 65 | if (filterOutRunsAfter.HasValue) 66 | { 67 | string date = filterOutRunsAfter.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); 68 | parameters.Add(string.Format("date={0}", 69 | Uri.EscapeDataString(date))); 70 | } 71 | 72 | if (variableFilters != null) 73 | { 74 | foreach (VariableValue variableValue in variableFilters) 75 | { 76 | if (variableValue != null) 77 | { 78 | parameters.Add(string.Format("var-{0}={1}", 79 | Uri.EscapeDataString(variableValue.VariableID), 80 | Uri.EscapeDataString(variableValue.ID))); 81 | } 82 | } 83 | } 84 | 85 | Uri innerUri = GetLeaderboardsUri(string.Format("{0}{1}", 86 | uri, 87 | parameters.ToParameters())); 88 | 89 | dynamic result = baseClient.DoRequest(innerUri); 90 | return Leaderboard.Parse(baseClient, result.data); 91 | } 92 | 93 | /// 94 | /// Fetch a Leaderboard object identified by the game ID and category ID. 95 | /// 96 | /// The ID for the game. 97 | /// The ID for the category. 98 | /// Optional. If included, will dictate the amount of top runs included in the response. 99 | /// Optional. If included, will filter runs by their platform. 100 | /// Optional. If included, will filter runs by their region. 101 | /// Optional. If included, will filter runs by their use of emulator. 102 | /// Optional. If included, will dictate whether runs without video are included in the response. 103 | /// Optional. If omitted, runs will be in the same order as the API. 104 | /// Optional. If included, will filter out runs performed after the specified DateTime. 105 | /// Optional. If included, will filter runs by the values present in specific variables. 106 | /// Optional. If included, will dictate the additional resources embedded in the response. 107 | /// 108 | public Leaderboard GetLeaderboardForFullGameCategory( 109 | string gameId, string categoryId, 110 | int? top = null, 111 | string platformId = null, string regionId = null, 112 | EmulatorsFilter emulatorsFilter = EmulatorsFilter.NotSet, bool filterOutRunsWithoutVideo = false, 113 | TimingMethod? orderBy = null, DateTime? filterOutRunsAfter = null, 114 | IEnumerable variableFilters = null, 115 | LeaderboardEmbeds embeds = default) 116 | { 117 | string uri = string.Format("/{0}/category/{1}", 118 | Uri.EscapeDataString(gameId), 119 | Uri.EscapeDataString(categoryId)); 120 | 121 | return getLeaderboard(uri, 122 | top, 123 | platformId, regionId, 124 | emulatorsFilter, filterOutRunsWithoutVideo, 125 | orderBy, filterOutRunsAfter, 126 | variableFilters, 127 | embeds); 128 | } 129 | 130 | /// 131 | /// Fetch a Leaderboard object identified by the game ID, level ID, and category ID. 132 | /// 133 | /// The ID for the game. 134 | /// The ID for the level. 135 | /// The ID for the category. 136 | /// Optional. If included, will dictate the amount of top runs included in the response. 137 | /// Optional. If included, will filter runs by their platform. 138 | /// Optional. If included, will filter runs by their region. 139 | /// Optional. If included, will filter runs by their use of emulator. 140 | /// Optional. If included, will dictate whether runs without video are included in the response. 141 | /// Optional. If omitted, runs will be in the same order as the API. 142 | /// Optional. If included, will filter out runs performed after the specified DateTime. 143 | /// Optional. If included, will filter runs by the values present in specific variables. 144 | /// Optional. If included, will dictate the additional resources embedded in the response. 145 | /// 146 | public Leaderboard GetLeaderboardForLevel( 147 | string gameId, string levelId, string categoryId, 148 | int? top = null, 149 | string platformId = null, string regionId = null, 150 | EmulatorsFilter emulatorsFilter = EmulatorsFilter.NotSet, bool filterOutRunsWithoutVideo = false, 151 | TimingMethod? orderBy = null, DateTime? filterOutRunsAfter = null, 152 | IEnumerable variableFilters = null, 153 | LeaderboardEmbeds embeds = default) 154 | { 155 | string uri = string.Format("/{0}/level/{1}/{2}", 156 | Uri.EscapeDataString(gameId), 157 | Uri.EscapeDataString(levelId), 158 | Uri.EscapeDataString(categoryId)); 159 | 160 | return getLeaderboard(uri, 161 | top, 162 | platformId, regionId, 163 | emulatorsFilter, filterOutRunsWithoutVideo, 164 | orderBy, filterOutRunsAfter, 165 | variableFilters, 166 | embeds); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Leaderboards/Record.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class Record : Run 6 | { 7 | public int Rank { get; private set; } 8 | 9 | private Record() { } 10 | 11 | public static new Record Parse(SpeedrunComClient client, dynamic recordElement) 12 | { 13 | var record = new Record 14 | { 15 | Rank = recordElement.place 16 | }; 17 | 18 | //Parse potential embeds 19 | 20 | var properties = recordElement.Properties as IDictionary; 21 | 22 | if (properties.ContainsKey("game")) 23 | { 24 | recordElement.run.game = recordElement.game; 25 | } 26 | 27 | if (properties.ContainsKey("category")) 28 | { 29 | recordElement.run.category = recordElement.category; 30 | } 31 | 32 | if (properties.ContainsKey("level")) 33 | { 34 | recordElement.run.level = recordElement.level; 35 | } 36 | 37 | if (properties.ContainsKey("players")) 38 | { 39 | recordElement.run.players = recordElement.players; 40 | } 41 | 42 | if (properties.ContainsKey("region")) 43 | { 44 | recordElement.run.region = recordElement.region; 45 | } 46 | 47 | if (properties.ContainsKey("platform")) 48 | { 49 | recordElement.run.platform = recordElement.platform; 50 | } 51 | 52 | Run.Parse(record, client, recordElement.run); 53 | 54 | return record; 55 | } 56 | 57 | public override int GetHashCode() 58 | { 59 | return (ID ?? string.Empty).GetHashCode(); 60 | } 61 | 62 | public override bool Equals(object obj) 63 | { 64 | if (obj is not Record other) 65 | { 66 | return false; 67 | } 68 | 69 | return ID == other.ID; 70 | } 71 | 72 | public override string ToString() 73 | { 74 | return string.Format("{0} - {1} in {2}", Game.Name, Category.Name, Times.Primary); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Levels/Level.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class Level : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public string Name { get; private set; } 13 | public Uri WebLink { get; private set; } 14 | public string Rules { get; private set; } 15 | 16 | #region Links 17 | 18 | private Lazy game; 19 | private Lazy> categories; 20 | private Lazy> variables; 21 | 22 | public string GameID { get; private set; } 23 | public Game Game => game.Value; 24 | public ReadOnlyCollection Categories => categories.Value; 25 | public ReadOnlyCollection Variables => variables.Value; 26 | public IEnumerable Runs { get; private set; } 27 | 28 | #endregion 29 | 30 | private Level() { } 31 | 32 | public static Level Parse(SpeedrunComClient client, dynamic levelElement) 33 | { 34 | if (levelElement is ArrayList) 35 | { 36 | return null; 37 | } 38 | 39 | var level = new Level 40 | { 41 | //Parse Attributes 42 | 43 | ID = levelElement.id as string, 44 | Name = levelElement.name as string, 45 | WebLink = new Uri(levelElement.weblink as string), 46 | Rules = levelElement.rules 47 | }; 48 | 49 | //Parse Links 50 | 51 | var properties = levelElement.Properties as IDictionary; 52 | var links = properties["links"] as IEnumerable; 53 | 54 | string gameUri = links.First(x => x.rel == "game").uri as string; 55 | level.GameID = gameUri[(gameUri.LastIndexOf('/') + 1)..]; 56 | level.game = new Lazy(() => client.Games.GetGame(level.GameID)); 57 | 58 | if (properties.ContainsKey("categories")) 59 | { 60 | Category categoryParser(dynamic x) 61 | { 62 | return Category.Parse(client, x) as Category; 63 | } 64 | 65 | ReadOnlyCollection categories = client.ParseCollection(levelElement.categories.data, (Func)categoryParser); 66 | level.categories = new Lazy>(() => categories); 67 | } 68 | else 69 | { 70 | level.categories = new Lazy>(() => client.Levels.GetCategories(level.ID)); 71 | } 72 | 73 | if (properties.ContainsKey("variables")) 74 | { 75 | Variable variableParser(dynamic x) 76 | { 77 | return Variable.Parse(client, x) as Variable; 78 | } 79 | 80 | ReadOnlyCollection variables = client.ParseCollection(levelElement.variables.data, (Func)variableParser); 81 | level.variables = new Lazy>(() => variables); 82 | } 83 | else 84 | { 85 | level.variables = new Lazy>(() => client.Levels.GetVariables(level.ID)); 86 | } 87 | 88 | level.Runs = client.Runs.GetRuns(levelId: level.ID); 89 | 90 | return level; 91 | } 92 | 93 | public override int GetHashCode() 94 | { 95 | return (ID ?? string.Empty).GetHashCode(); 96 | } 97 | 98 | public override bool Equals(object obj) 99 | { 100 | if (obj is not Level other) 101 | { 102 | return false; 103 | } 104 | 105 | return ID == other.ID; 106 | } 107 | 108 | public override string ToString() 109 | { 110 | return Name; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Levels/LevelEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct LevelEmbeds 4 | { 5 | private Embeds embeds; 6 | 7 | public bool EmbedCategories 8 | { 9 | get => embeds["categories"]; 10 | set => embeds["categories"] = value; 11 | } 12 | public bool EmbedVariables 13 | { 14 | get => embeds["variables"]; 15 | set => embeds["variables"] = value; 16 | } 17 | 18 | /// 19 | /// Options for embedding resources in Level responses. 20 | /// 21 | /// Dictates whether a Collection of Category objects is included in the response. 22 | /// Dictates whether a Collection of Variable objects is included in the response. 23 | public LevelEmbeds( 24 | bool embedCategories = false, 25 | bool embedVariables = false) 26 | { 27 | embeds = new Embeds(); 28 | EmbedCategories = embedCategories; 29 | EmbedVariables = embedVariables; 30 | } 31 | 32 | public override string ToString() 33 | { 34 | return embeds.ToString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Levels/LevelsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class LevelsClient 8 | { 9 | public const string Name = "levels"; 10 | 11 | private readonly SpeedrunComClient baseClient; 12 | 13 | public LevelsClient(SpeedrunComClient baseClient) 14 | { 15 | this.baseClient = baseClient; 16 | } 17 | 18 | public static Uri GetLevelsUri(string subUri) 19 | { 20 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 21 | } 22 | 23 | /// 24 | /// Fetch a Level object identified by its URI. 25 | /// 26 | /// The site URI for the level. 27 | /// Optional. If included, will dictate the embedded resources included in the response. 28 | /// 29 | public Level GetLevelFromSiteUri(string siteUri, LevelEmbeds embeds = default) 30 | { 31 | string id = GetLevelIDFromSiteUri(siteUri); 32 | 33 | if (string.IsNullOrEmpty(id)) 34 | { 35 | return null; 36 | } 37 | 38 | return GetLevel(id, embeds); 39 | } 40 | 41 | /// 42 | /// Fetch a Level ID identified by its URI. 43 | /// 44 | /// The site URI for the level. 45 | /// 46 | public string GetLevelIDFromSiteUri(string siteUri) 47 | { 48 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 49 | 50 | if (elementDescription == null 51 | || elementDescription.Type != ElementType.Level) 52 | { 53 | return null; 54 | } 55 | 56 | return elementDescription.ID; 57 | } 58 | 59 | /// 60 | /// Fetch a Level object identified by its ID. 61 | /// 62 | /// The ID for the level. 63 | /// Optional. If included, will dictate the embedded resources included in the response. 64 | /// 65 | public Level GetLevel(string levelId, 66 | LevelEmbeds embeds = default) 67 | { 68 | var parameters = new List() { embeds.ToString() }; 69 | 70 | Uri uri = GetLevelsUri(string.Format("/{0}{1}", 71 | Uri.EscapeDataString(levelId), 72 | parameters.ToParameters())); 73 | 74 | dynamic result = baseClient.DoRequest(uri); 75 | 76 | return Level.Parse(baseClient, result.data); 77 | } 78 | 79 | /// 80 | /// Fetch a Collection of Category objects from a level's ID. 81 | /// 82 | /// The ID for the level. 83 | /// Optional. If included, will dictate whether miscellaneous categories are included. 84 | /// Optional. If included, will dictate the additional resources embedded in the response. 85 | /// Optional. If omitted, categories will be in the same order as the API. 86 | /// 87 | public ReadOnlyCollection GetCategories( 88 | string levelId, bool miscellaneous = true, 89 | CategoryEmbeds embeds = default, 90 | CategoriesOrdering orderBy = default) 91 | { 92 | var parameters = new List() { embeds.ToString() }; 93 | 94 | parameters.AddRange(orderBy.ToParameters()); 95 | 96 | if (!miscellaneous) 97 | { 98 | parameters.Add("miscellaneous=no"); 99 | } 100 | 101 | Uri uri = GetLevelsUri(string.Format("/{0}/categories{1}", 102 | Uri.EscapeDataString(levelId), 103 | parameters.ToParameters())); 104 | 105 | return baseClient.DoDataCollectionRequest(uri, 106 | x => Category.Parse(baseClient, x)); 107 | } 108 | 109 | /// 110 | /// Fetch a Collection of Variable objects from a level's ID. 111 | /// 112 | /// The ID for the level. 113 | /// Optional. If omitted, variables will be in the same order as the API. 114 | /// 115 | public ReadOnlyCollection GetVariables(string levelId, 116 | VariablesOrdering orderBy = default) 117 | { 118 | var parameters = new List(orderBy.ToParameters()); 119 | 120 | Uri uri = GetLevelsUri(string.Format("/{0}/variables{1}", 121 | Uri.EscapeDataString(levelId), 122 | parameters.ToParameters())); 123 | 124 | return baseClient.DoDataCollectionRequest(uri, 125 | x => Variable.Parse(baseClient, x)); 126 | } 127 | 128 | /// 129 | /// Fetch a Leaderboard object from a level's ID. 130 | /// 131 | /// The ID for the level. 132 | /// Optional. If included, will dictate the amount of top runs included in the response. 133 | /// Optional. If included, will dictate whether or not empty leaderboards are included in the response. 134 | /// Optional. If included, will dictate the amount of elements included in each pagination. 135 | /// Optional. If included, will dictate the additional resources embedded in the response. 136 | /// 137 | public IEnumerable GetRecords(string levelId, 138 | int? top = null, bool skipEmptyLeaderboards = false, 139 | int? elementsPerPage = null, 140 | LeaderboardEmbeds embeds = default) 141 | { 142 | var parameters = new List() { embeds.ToString() }; 143 | 144 | if (top.HasValue) 145 | { 146 | parameters.Add(string.Format("top={0}", top.Value)); 147 | } 148 | 149 | if (skipEmptyLeaderboards) 150 | { 151 | parameters.Add("skip-empty=true"); 152 | } 153 | 154 | if (elementsPerPage.HasValue) 155 | { 156 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 157 | } 158 | 159 | Uri uri = GetLevelsUri(string.Format("/{0}/records{1}", 160 | Uri.EscapeDataString(levelId), 161 | parameters.ToParameters())); 162 | 163 | return baseClient.DoPaginatedRequest(uri, 164 | x => Leaderboard.Parse(baseClient, x)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Levels/LevelsOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Levels in responses. 7 | /// 8 | public enum LevelsOrdering : int 9 | { 10 | Position = 0, 11 | PositionDescending, 12 | Name, 13 | NameDescending 14 | } 15 | 16 | internal static class LevelsOrderingHelpers 17 | { 18 | internal static IEnumerable ToParameters(this LevelsOrdering ordering) 19 | { 20 | bool isDescending = ((int)ordering & 1) == 1; 21 | if (isDescending) 22 | { 23 | ordering = (LevelsOrdering)((int)ordering - 1); 24 | } 25 | 26 | string str = ""; 27 | 28 | switch (ordering) 29 | { 30 | case LevelsOrdering.Name: 31 | str = "name"; break; 32 | } 33 | 34 | var list = new List(); 35 | 36 | if (!string.IsNullOrEmpty(str)) 37 | { 38 | list.Add(string.Format("orderby={0}", str)); 39 | } 40 | 41 | if (isDescending) 42 | { 43 | list.Add("direction=desc"); 44 | } 45 | 46 | return list; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/NotAuthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class NotAuthorizedException : Exception 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Notifications/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace SpeedrunComSharp; 7 | 8 | public class Notification : IElementWithID 9 | { 10 | public string ID { get; private set; } 11 | public DateTime TimeCreated { get; private set; } 12 | public NotificationStatus Status { get; private set; } 13 | public string Text { get; private set; } 14 | 15 | public NotificationType Type { get; private set; } 16 | public Uri WebLink { get; private set; } 17 | 18 | #region Links 19 | 20 | private Lazy run; 21 | private Lazy game; 22 | 23 | public string RunID { get; private set; } 24 | public Run Run => run.Value; 25 | 26 | public string GameID { get; private set; } 27 | public Game Game => game.Value; 28 | 29 | #endregion 30 | 31 | private Notification() { } 32 | 33 | public static Notification Parse(SpeedrunComClient client, dynamic notificationElement) 34 | { 35 | var notification = new Notification 36 | { 37 | //Parse Attributes 38 | 39 | ID = notificationElement.id as string, 40 | TimeCreated = DateTime.Parse(notificationElement.created as string, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), 41 | Status = NotificationStatusHelpers.Parse(notificationElement.status as string), 42 | Text = notificationElement.text as string, 43 | Type = NotificationTypeHelpers.Parse(notificationElement.item.rel as string), 44 | WebLink = new Uri(notificationElement.item.uri as string) 45 | }; 46 | 47 | //Parse Links 48 | 49 | var links = notificationElement.links as IList; 50 | 51 | if (links != null) 52 | { 53 | dynamic run = links.FirstOrDefault(x => x.rel == "run"); 54 | 55 | if (run != null) 56 | { 57 | string runUri = run.uri as string; 58 | notification.RunID = runUri[(runUri.LastIndexOf("/") + 1)..]; 59 | } 60 | 61 | dynamic game = links.FirstOrDefault(x => x.rel == "game"); 62 | 63 | if (game != null) 64 | { 65 | string gameUri = game.uri as string; 66 | notification.GameID = gameUri[(gameUri.LastIndexOf("/") + 1)..]; 67 | } 68 | } 69 | 70 | if (!string.IsNullOrEmpty(notification.RunID)) 71 | { 72 | notification.run = new Lazy(() => client.Runs.GetRun(notification.RunID)); 73 | } 74 | else 75 | { 76 | notification.run = new Lazy(() => null); 77 | } 78 | 79 | if (!string.IsNullOrEmpty(notification.GameID)) 80 | { 81 | notification.game = new Lazy(() => client.Games.GetGame(notification.GameID)); 82 | } 83 | else 84 | { 85 | notification.game = new Lazy(() => null); 86 | } 87 | 88 | return notification; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Notifications/NotificationStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public enum NotificationStatus 6 | { 7 | Unread, Read 8 | } 9 | 10 | public static class NotificationStatusHelpers 11 | { 12 | public static NotificationStatus Parse(string status) 13 | { 14 | return status switch 15 | { 16 | "read" => NotificationStatus.Read, 17 | "unread" => NotificationStatus.Unread, 18 | _ => throw new ArgumentException("status"), 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Notifications/NotificationType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public enum NotificationType 6 | { 7 | Post, Run, Game, Guide, Thread, Resource, Moderator 8 | } 9 | 10 | public static class NotificationTypeHelpers 11 | { 12 | public static NotificationType Parse(string type) 13 | { 14 | return type switch 15 | { 16 | "post" => NotificationType.Post, 17 | "run" => NotificationType.Run, 18 | "game" => NotificationType.Game, 19 | "guide" => NotificationType.Guide, 20 | "thread" => NotificationType.Thread, 21 | "resource" => NotificationType.Resource, 22 | "moderator" => NotificationType.Moderator, 23 | _ => throw new ArgumentException("type"), 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Notifications/NotificationsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class NotificationsClient 7 | { 8 | public const string Name = "notifications"; 9 | 10 | private readonly SpeedrunComClient baseClient; 11 | 12 | public NotificationsClient(SpeedrunComClient baseClient) 13 | { 14 | this.baseClient = baseClient; 15 | } 16 | 17 | public static Uri GetNotificationsUri(string subUri) 18 | { 19 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 20 | } 21 | 22 | /// 23 | /// Fetch a Collection of Notification objects. Authentication is required for this action. 24 | /// 25 | /// Optional. If included, will dictate the amount of elements included in each pagination. 26 | /// Optional. If omitted, notifications will be from newest to oldest. 27 | /// 28 | public IEnumerable GetNotifications( 29 | int? elementsPerPage = null, 30 | NotificationsOrdering ordering = default) 31 | { 32 | var parameters = new List(); 33 | 34 | if (elementsPerPage.HasValue) 35 | { 36 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 37 | } 38 | 39 | parameters.AddRange(ordering.ToParameters()); 40 | 41 | Uri uri = GetNotificationsUri(string.Format("{0}", 42 | parameters.ToParameters())); 43 | 44 | return baseClient.DoPaginatedRequest(uri, 45 | x => Notification.Parse(baseClient, x)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Notifications/NotificationsOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Notifications in responses. 7 | /// 8 | public enum NotificationsOrdering : int 9 | { 10 | NewestToOldest = 0, 11 | OldestToNewest 12 | } 13 | 14 | internal static class NotificationsOrderingHelpers 15 | { 16 | internal static IEnumerable ToParameters(this NotificationsOrdering ordering) 17 | { 18 | var list = new List(); 19 | 20 | if (ordering == NotificationsOrdering.OldestToNewest) 21 | { 22 | list.Add("direction=asc"); 23 | } 24 | 25 | return list; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Platforms/Platform.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class Platform : IElementWithID 7 | { 8 | public string ID { get; private set; } 9 | public string Name { get; private set; } 10 | public int YearOfRelease { get; private set; } 11 | 12 | #region Links 13 | 14 | public IEnumerable Games { get; private set; } 15 | public IEnumerable Runs { get; private set; } 16 | 17 | #endregion 18 | 19 | private Platform() { } 20 | 21 | public static Platform Parse(SpeedrunComClient client, dynamic platformElement) 22 | { 23 | if (platformElement is ArrayList) 24 | { 25 | return null; 26 | } 27 | 28 | var platform = new Platform 29 | { 30 | //Parse Attributes 31 | 32 | ID = platformElement.id as string, 33 | Name = platformElement.name as string, 34 | YearOfRelease = (int)platformElement.released 35 | }; 36 | 37 | //Parse Links 38 | 39 | platform.Games = client.Games.GetGames(platformId: platform.ID); 40 | platform.Runs = client.Runs.GetRuns(platformId: platform.ID); 41 | 42 | return platform; 43 | } 44 | 45 | public override int GetHashCode() 46 | { 47 | return (ID ?? string.Empty).GetHashCode(); 48 | } 49 | 50 | public override bool Equals(object obj) 51 | { 52 | if (obj is not Platform other) 53 | { 54 | return false; 55 | } 56 | 57 | return ID == other.ID; 58 | } 59 | 60 | public override string ToString() 61 | { 62 | return Name; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Platforms/PlatformsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class PlatformsClient 7 | { 8 | public const string Name = "platforms"; 9 | 10 | private readonly SpeedrunComClient baseClient; 11 | 12 | public PlatformsClient(SpeedrunComClient baseClient) 13 | { 14 | this.baseClient = baseClient; 15 | } 16 | 17 | public static Uri GetPlatformsUri(string subUri) 18 | { 19 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 20 | } 21 | 22 | /// 23 | /// Fetch a Platform object identified by its URI. 24 | /// 25 | /// The site URI for the platform. 26 | /// 27 | public Platform GetPlatformFromSiteUri(string siteUri) 28 | { 29 | string id = GetPlatformIDFromSiteUri(siteUri); 30 | 31 | if (string.IsNullOrEmpty(id)) 32 | { 33 | return null; 34 | } 35 | 36 | return GetPlatform(id); 37 | } 38 | 39 | /// 40 | /// Fetch a Platform ID identified by its URI. 41 | /// 42 | /// The site URI for the platform. 43 | /// 44 | public string GetPlatformIDFromSiteUri(string siteUri) 45 | { 46 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 47 | 48 | if (elementDescription == null 49 | || elementDescription.Type != ElementType.Platform) 50 | { 51 | return null; 52 | } 53 | 54 | return elementDescription.ID; 55 | } 56 | 57 | /// 58 | /// Fetch a Collection of Platform objects. 59 | /// 60 | /// Optional. If included, will dictate the amount of elements included in each pagination. 61 | /// Optional. If omitted, platforms will be in the same order as the API. 62 | /// 63 | public IEnumerable GetPlatforms(int? elementsPerPage = null, 64 | PlatformsOrdering orderBy = default) 65 | { 66 | var parameters = new List(); 67 | 68 | parameters.AddRange(orderBy.ToParameters()); 69 | 70 | if (elementsPerPage.HasValue) 71 | { 72 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 73 | } 74 | 75 | Uri uri = GetPlatformsUri(parameters.ToParameters()); 76 | 77 | return baseClient.DoPaginatedRequest(uri, 78 | x => Platform.Parse(baseClient, x) as Platform); 79 | } 80 | 81 | /// 82 | /// Fetch a Platform object identified by its ID. 83 | /// 84 | /// The ID for the platform. 85 | /// 86 | public Platform GetPlatform(string platformId) 87 | { 88 | Uri uri = GetPlatformsUri(string.Format("/{0}", Uri.EscapeDataString(platformId))); 89 | dynamic result = baseClient.DoRequest(uri); 90 | 91 | return Platform.Parse(baseClient, result.data); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Platforms/PlatformsOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Platforms in responses. 7 | /// 8 | public enum PlatformsOrdering : int 9 | { 10 | Name = 0, 11 | NameDescending, 12 | YearOfRelease, 13 | YearOfReleaseDescending 14 | } 15 | 16 | internal static class PlatformsOrderingHelpers 17 | { 18 | internal static IEnumerable ToParameters(this PlatformsOrdering ordering) 19 | { 20 | bool isDescending = ((int)ordering & 1) == 1; 21 | if (isDescending) 22 | { 23 | ordering = (PlatformsOrdering)((int)ordering - 1); 24 | } 25 | 26 | string str = ""; 27 | 28 | switch (ordering) 29 | { 30 | case PlatformsOrdering.YearOfRelease: 31 | str = "released"; break; 32 | } 33 | 34 | var list = new List(); 35 | 36 | if (!string.IsNullOrEmpty(str)) 37 | { 38 | list.Add(string.Format("orderby={0}", str)); 39 | } 40 | 41 | if (isDescending) 42 | { 43 | list.Add("direction=desc"); 44 | } 45 | 46 | return list; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/PotentialEmbed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | internal class PotentialEmbed 6 | where T : IElementWithID 7 | { 8 | public Lazy Object { get; private set; } 9 | public string ID { get; private set; } 10 | 11 | private PotentialEmbed() { } 12 | 13 | public static PotentialEmbed Parse(dynamic element, Func objectQuery, Func objectParser) 14 | { 15 | var potentialEmbed = new PotentialEmbed(); 16 | 17 | if (element == null) 18 | { 19 | potentialEmbed.Object = new Lazy(() => default); 20 | } 21 | else if (element is string) 22 | { 23 | potentialEmbed.ID = element as string; 24 | potentialEmbed.Object = new Lazy(() => objectQuery(potentialEmbed.ID)); 25 | } 26 | else 27 | { 28 | dynamic parsedObject = objectParser(element.data); 29 | potentialEmbed.Object = new Lazy(() => parsedObject); 30 | if (parsedObject != null) 31 | { 32 | potentialEmbed.ID = parsedObject.ID; 33 | } 34 | } 35 | 36 | return potentialEmbed; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Regions/Region.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class Region : IElementWithID 7 | { 8 | public string ID { get; private set; } 9 | public string Name { get; private set; } 10 | 11 | public string Abbreviation 12 | { 13 | get 14 | { 15 | return Name switch 16 | { 17 | "USA / NTSC" => "NTSC-U", 18 | "EUR / PAL" => "PAL", 19 | "JPN / NTSC" => "NTSC-J", 20 | "CHN / iQue" => "CHN", 21 | "KOR / NTSC" => "KOR", 22 | _ => Name, 23 | }; 24 | } 25 | } 26 | 27 | #region Links 28 | 29 | public IEnumerable Games { get; private set; } 30 | public IEnumerable Runs { get; private set; } 31 | 32 | #endregion 33 | 34 | private Region() { } 35 | 36 | public static Region Parse(SpeedrunComClient client, dynamic regionElement) 37 | { 38 | if (regionElement is ArrayList) 39 | { 40 | return null; 41 | } 42 | 43 | var region = new Region 44 | { 45 | //Parse Attributes 46 | 47 | ID = regionElement.id as string, 48 | Name = regionElement.name as string 49 | }; 50 | 51 | //Parse Links 52 | 53 | region.Games = client.Games.GetGames(regionId: region.ID); 54 | region.Runs = client.Runs.GetRuns(regionId: region.ID); 55 | 56 | return region; 57 | } 58 | 59 | public override int GetHashCode() 60 | { 61 | return (ID ?? string.Empty).GetHashCode(); 62 | } 63 | 64 | public override bool Equals(object obj) 65 | { 66 | if (obj is not Region region) 67 | { 68 | return false; 69 | } 70 | 71 | return ID == region.ID; 72 | } 73 | 74 | public override string ToString() 75 | { 76 | return Name; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Regions/RegionsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | namespace SpeedrunComSharp; 4 | 5 | public class RegionsClient 6 | { 7 | public const string Name = "regions"; 8 | 9 | private readonly SpeedrunComClient baseClient; 10 | 11 | public RegionsClient(SpeedrunComClient baseClient) 12 | { 13 | this.baseClient = baseClient; 14 | } 15 | 16 | public static Uri GetRegionsUri(string subUri) 17 | { 18 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 19 | } 20 | 21 | /// 22 | /// Fetch a Region object identified by its URI. 23 | /// 24 | /// The site URI for the region. 25 | /// 26 | public Region GetRegionFromSiteUri(string siteUri) 27 | { 28 | string id = GetRegionIDFromSiteUri(siteUri); 29 | 30 | if (string.IsNullOrEmpty(id)) 31 | { 32 | return null; 33 | } 34 | 35 | return GetRegion(id); 36 | } 37 | 38 | /// 39 | /// Fetch a Region ID identified by its URI. 40 | /// 41 | /// The site URI for the region. 42 | /// 43 | public string GetRegionIDFromSiteUri(string siteUri) 44 | { 45 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 46 | 47 | if (elementDescription == null 48 | || elementDescription.Type != ElementType.Region) 49 | { 50 | return null; 51 | } 52 | 53 | return elementDescription.ID; 54 | } 55 | 56 | /// 57 | /// Fetch a Collection of Region objects. 58 | /// 59 | /// Optional. If included, will dictate the amount of elements included in each pagination. 60 | /// Optional. If omitted, regions will be in the same order as the API. 61 | /// 62 | public IEnumerable GetRegions(int? elementsPerPage = null, 63 | RegionsOrdering orderBy = default) 64 | { 65 | var parameters = new List(); 66 | 67 | parameters.AddRange(orderBy.ToParameters()); 68 | 69 | if (elementsPerPage.HasValue) 70 | { 71 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 72 | } 73 | 74 | Uri uri = GetRegionsUri(parameters.ToParameters()); 75 | 76 | return baseClient.DoPaginatedRequest(uri, 77 | x => Region.Parse(baseClient, x) as Region); 78 | } 79 | 80 | /// 81 | /// Fetch a Region object identified by its ID. 82 | /// 83 | /// The ID for the region. 84 | /// 85 | public Region GetRegion(string regionId) 86 | { 87 | Uri uri = GetRegionsUri(string.Format("/{0}", Uri.EscapeDataString(regionId))); 88 | dynamic result = baseClient.DoRequest(uri); 89 | 90 | return Region.Parse(baseClient, result.data); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Regions/RegionsOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Regions in responses. 7 | /// 8 | public enum RegionsOrdering : int 9 | { 10 | Name = 0, 11 | NameDescending, 12 | } 13 | 14 | internal static class RegionsOrderingHelpers 15 | { 16 | internal static IEnumerable ToParameters(this RegionsOrdering ordering) 17 | { 18 | bool isDescending = ((int)ordering & 1) == 1; 19 | if (isDescending) 20 | { 21 | ordering = (RegionsOrdering)((int)ordering - 1); 22 | } 23 | 24 | string str = ""; 25 | 26 | /*switch (ordering) 27 | { 28 | }*/ 29 | 30 | var list = new List(); 31 | 32 | if (!string.IsNullOrEmpty(str)) 33 | { 34 | list.Add(string.Format("orderby={0}", str)); 35 | } 36 | 37 | if (isDescending) 38 | { 39 | list.Add("direction=desc"); 40 | } 41 | 42 | return list; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/Run.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class Run : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public Uri WebLink { get; private set; } 13 | public string GameID { get; private set; } 14 | public string LevelID { get; private set; } 15 | public string CategoryID { get; private set; } 16 | public RunVideos Videos { get; private set; } 17 | public string Comment { get; private set; } 18 | public RunStatus Status { get; private set; } 19 | public Player Player => Players.FirstOrDefault(); 20 | public ReadOnlyCollection Players { get; internal set; } 21 | public DateTime? Date { get; private set; } 22 | public DateTime? DateSubmitted { get; private set; } 23 | public RunTimes Times { get; private set; } 24 | public RunSystem System { get; private set; } 25 | public Uri SplitsUri { get; private set; } 26 | public bool SplitsAvailable => SplitsUri != null; 27 | public ReadOnlyCollection VariableValues { get; private set; } 28 | 29 | #region Links 30 | 31 | internal Lazy game; 32 | internal Lazy category; 33 | private Lazy level; 34 | private Lazy examiner; 35 | 36 | public Game Game => game.Value; 37 | public Category Category => category.Value; 38 | public Level Level => level.Value; 39 | public Platform Platform => System.Platform; 40 | public Region Region => System.Region; 41 | public User Examiner => examiner.Value; 42 | 43 | #endregion 44 | 45 | protected Run() { } 46 | 47 | internal static void Parse(Run run, SpeedrunComClient client, dynamic runElement) 48 | { 49 | //Parse Attributes 50 | 51 | run.ID = runElement.id as string; 52 | run.WebLink = new Uri(runElement.weblink as string); 53 | run.Videos = RunVideos.Parse(client, runElement.videos) as RunVideos; 54 | run.Comment = runElement.comment as string; 55 | run.Status = RunStatus.Parse(client, runElement.status) as RunStatus; 56 | 57 | Player parsePlayer(dynamic x) 58 | { 59 | return Player.Parse(client, x) as Player; 60 | } 61 | 62 | if (runElement.players is IEnumerable) 63 | { 64 | run.Players = client.ParseCollection(runElement.players, (Func)parsePlayer); 65 | } 66 | else if (runElement.players is System.Collections.ArrayList && runElement.players.Count == 0) 67 | { 68 | run.Players = new List().AsReadOnly(); 69 | } 70 | else 71 | { 72 | run.Players = client.ParseCollection(runElement.players.data, (Func)parsePlayer); 73 | } 74 | 75 | dynamic runDate = runElement.date; 76 | if (!string.IsNullOrEmpty(runDate)) 77 | { 78 | run.Date = DateTime.Parse(runDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 79 | } 80 | 81 | dynamic dateSubmitted = runElement.submitted; 82 | if (!string.IsNullOrEmpty(dateSubmitted)) 83 | { 84 | run.DateSubmitted = DateTime.Parse(dateSubmitted, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 85 | } 86 | 87 | run.Times = RunTimes.Parse(client, runElement.times) as RunTimes; 88 | run.System = RunSystem.Parse(client, runElement.system) as RunSystem; 89 | 90 | dynamic splits = runElement.splits; 91 | if (splits != null) 92 | { 93 | run.SplitsUri = new Uri(splits.uri as string); 94 | } 95 | 96 | if (runElement.values is DynamicJsonObject) 97 | { 98 | var valueProperties = runElement.values.Properties as IDictionary; 99 | run.VariableValues = valueProperties.Select(x => VariableValue.ParseValueDescriptor(client, x)).ToList().AsReadOnly(); 100 | } 101 | else 102 | { 103 | run.VariableValues = new List().AsReadOnly(); 104 | } 105 | 106 | //Parse Links 107 | 108 | var properties = runElement.Properties as IDictionary; 109 | 110 | if (properties["game"] is string) 111 | { 112 | run.GameID = runElement.game as string; 113 | run.game = new Lazy(() => client.Games.GetGame(run.GameID)); 114 | } 115 | else 116 | { 117 | var game = Game.Parse(client, properties["game"].data) as Game; 118 | run.game = new Lazy(() => game); 119 | run.GameID = game.ID; 120 | } 121 | 122 | if (properties["category"] == null) 123 | { 124 | run.category = new Lazy(() => null); 125 | } 126 | else if (properties["category"] is string) 127 | { 128 | run.CategoryID = runElement.category as string; 129 | run.category = new Lazy(() => client.Categories.GetCategory(run.CategoryID)); 130 | } 131 | else 132 | { 133 | var category = Category.Parse(client, properties["category"].data) as Category; 134 | run.category = new Lazy(() => category); 135 | if (category != null) 136 | { 137 | run.CategoryID = category.ID; 138 | } 139 | } 140 | 141 | if (properties["level"] == null) 142 | { 143 | run.level = new Lazy(() => null); 144 | } 145 | else if (properties["level"] is string) 146 | { 147 | run.LevelID = runElement.level as string; 148 | run.level = new Lazy(() => client.Levels.GetLevel(run.LevelID)); 149 | } 150 | else 151 | { 152 | var level = Level.Parse(client, properties["level"].data) as Level; 153 | run.level = new Lazy(() => level); 154 | if (level != null) 155 | { 156 | run.LevelID = level.ID; 157 | } 158 | } 159 | 160 | if (properties.ContainsKey("platform")) 161 | { 162 | var platform = Platform.Parse(client, properties["platform"].data) as Platform; 163 | run.System.platform = new Lazy(() => platform); 164 | } 165 | 166 | if (properties.ContainsKey("region")) 167 | { 168 | var region = Region.Parse(client, properties["region"].data) as Region; 169 | run.System.region = new Lazy(() => region); 170 | } 171 | 172 | if (!string.IsNullOrEmpty(run.Status.ExaminerUserID)) 173 | { 174 | run.examiner = new Lazy(() => client.Users.GetUser(run.Status.ExaminerUserID)); 175 | } 176 | else 177 | { 178 | run.examiner = new Lazy(() => null); 179 | } 180 | } 181 | 182 | public static Run Parse(SpeedrunComClient client, dynamic runElement) 183 | { 184 | var run = new Run(); 185 | 186 | Parse(run, client, runElement); 187 | 188 | return run; 189 | } 190 | 191 | public override int GetHashCode() 192 | { 193 | return (ID ?? string.Empty).GetHashCode(); 194 | } 195 | 196 | public override bool Equals(object obj) 197 | { 198 | if (obj is not Run other) 199 | { 200 | return false; 201 | } 202 | 203 | return ID == other.ID; 204 | } 205 | 206 | public override string ToString() 207 | { 208 | return string.Format("{0} - {1} in {2}", Game.Name, Category.Name, Times.Primary); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct RunEmbeds 4 | { 5 | private Embeds embeds; 6 | 7 | public bool EmbedGame 8 | { 9 | get => embeds["game"]; 10 | set => embeds["game"] = value; 11 | } 12 | 13 | public bool EmbedCategory 14 | { 15 | get => embeds["category"]; 16 | set => embeds["category"] = value; 17 | } 18 | 19 | public bool EmbedLevel 20 | { 21 | get => embeds["level"]; 22 | set => embeds["level"] = value; 23 | } 24 | 25 | public bool EmbedPlayers 26 | { 27 | get => embeds["players"]; 28 | set => embeds["players"] = value; 29 | } 30 | 31 | public bool EmbedRegion 32 | { 33 | get => embeds["region"]; 34 | set => embeds["region"] = value; 35 | } 36 | 37 | public bool EmbedPlatform 38 | { 39 | get => embeds["platform"]; 40 | set => embeds["platform"] = value; 41 | } 42 | 43 | /// 44 | /// Options for embedding resources in Run responses. 45 | /// 46 | /// Dictates whether a Game object is included in the response. 47 | /// Dictates whether a Category object is included in the response. 48 | /// Dictates whether a Level object is included in the response. 49 | /// Dictates whether a Collection of Runner objects containing each runner is included in the response. 50 | /// Dictates whether a Region object is included in the response. 51 | /// Dictates whether a Platform object is included in the response. 52 | public RunEmbeds( 53 | bool embedGame = false, 54 | bool embedCategory = false, 55 | bool embedLevel = false, 56 | bool embedPlayers = false, 57 | bool embedRegion = false, 58 | bool embedPlatform = false) 59 | { 60 | embeds = new Embeds(); 61 | EmbedGame = embedGame; 62 | EmbedCategory = embedCategory; 63 | EmbedLevel = embedLevel; 64 | EmbedPlayers = embedPlayers; 65 | EmbedRegion = embedRegion; 66 | EmbedPlatform = embedPlatform; 67 | } 68 | 69 | public override string ToString() 70 | { 71 | return embeds.ToString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class RunStatus 8 | { 9 | public RunStatusType Type { get; private set; } 10 | public string ExaminerUserID { get; private set; } 11 | public string Reason { get; private set; } 12 | public DateTime? VerifyDate { get; private set; } 13 | 14 | #region Links 15 | 16 | private Lazy examiner; 17 | 18 | public User Examiner => examiner.Value; 19 | 20 | #endregion 21 | 22 | private RunStatus() { } 23 | 24 | private static RunStatusType ParseType(string type) 25 | { 26 | return type switch 27 | { 28 | "new" => RunStatusType.New, 29 | "verified" => RunStatusType.Verified, 30 | "rejected" => RunStatusType.Rejected, 31 | _ => throw new ArgumentException("type"), 32 | }; 33 | } 34 | 35 | public static RunStatus Parse(SpeedrunComClient client, dynamic statusElement) 36 | { 37 | var status = new RunStatus(); 38 | 39 | var properties = statusElement.Properties as IDictionary; 40 | 41 | status.Type = ParseType(statusElement.status as string); 42 | 43 | if (status.Type is RunStatusType.Rejected 44 | or RunStatusType.Verified) 45 | { 46 | status.ExaminerUserID = statusElement.examiner as string; 47 | status.examiner = new Lazy(() => client.Users.GetUser(status.ExaminerUserID)); 48 | 49 | if (status.Type == RunStatusType.Verified) 50 | { 51 | string date = properties["verify-date"] as string; 52 | if (!string.IsNullOrEmpty(date)) 53 | { 54 | status.VerifyDate = DateTime.Parse(date, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 55 | } 56 | } 57 | } 58 | else 59 | { 60 | status.examiner = new Lazy(() => null); 61 | } 62 | 63 | if (status.Type == RunStatusType.Rejected) 64 | { 65 | status.Reason = statusElement.reason as string; 66 | } 67 | 68 | return status; 69 | } 70 | 71 | public override string ToString() 72 | { 73 | if (Type == RunStatusType.Rejected) 74 | { 75 | return "Rejected:" + Reason; 76 | } 77 | else 78 | { 79 | return Type.ToString(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunStatusType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum RunStatusType 4 | { 5 | New, Verified, Rejected 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class RunSystem 6 | { 7 | public string PlatformID { get; private set; } 8 | public bool IsEmulated { get; private set; } 9 | public string RegionID { get; private set; } 10 | 11 | #region Links 12 | 13 | internal Lazy platform; 14 | internal Lazy region; 15 | 16 | public Platform Platform => platform.Value; 17 | public Region Region => region.Value; 18 | 19 | #endregion 20 | 21 | private RunSystem() { } 22 | 23 | public static RunSystem Parse(SpeedrunComClient client, dynamic systemElement) 24 | { 25 | var system = new RunSystem 26 | { 27 | IsEmulated = (bool)systemElement.emulated 28 | }; 29 | 30 | if (!string.IsNullOrEmpty(systemElement.platform as string)) 31 | { 32 | system.PlatformID = systemElement.platform as string; 33 | system.platform = new Lazy(() => client.Platforms.GetPlatform(system.PlatformID)); 34 | } 35 | else 36 | { 37 | system.platform = new Lazy(() => null); 38 | } 39 | 40 | if (!string.IsNullOrEmpty(systemElement.region as string)) 41 | { 42 | system.RegionID = systemElement.region as string; 43 | system.region = new Lazy(() => client.Regions.GetRegion(system.RegionID)); 44 | } 45 | else 46 | { 47 | system.region = new Lazy(() => null); 48 | } 49 | 50 | return system; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunTimes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class RunTimes 6 | { 7 | public TimeSpan? Primary { get; private set; } 8 | public TimeSpan? RealTime { get; private set; } 9 | public TimeSpan? RealTimeWithoutLoads { get; private set; } 10 | public TimeSpan? GameTime { get; private set; } 11 | 12 | private RunTimes() { } 13 | 14 | public static RunTimes Parse(SpeedrunComClient client, dynamic timesElement) 15 | { 16 | var times = new RunTimes(); 17 | 18 | if (timesElement.primary != null) 19 | { 20 | times.Primary = TimeSpan.FromSeconds((double)timesElement.primary_t); 21 | } 22 | 23 | if (timesElement.realtime != null) 24 | { 25 | times.RealTime = TimeSpan.FromSeconds((double)timesElement.realtime_t); 26 | } 27 | 28 | if (timesElement.realtime_noloads != null) 29 | { 30 | times.RealTimeWithoutLoads = TimeSpan.FromSeconds((double)timesElement.realtime_noloads_t); 31 | } 32 | 33 | if (timesElement.ingame != null) 34 | { 35 | times.GameTime = TimeSpan.FromSeconds((double)timesElement.ingame_t); 36 | } 37 | 38 | return times; 39 | 40 | } 41 | 42 | public override string ToString() 43 | { 44 | if (Primary.HasValue) 45 | { 46 | return Primary.Value.ToString(); 47 | } 48 | else 49 | { 50 | return "-"; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunVideos.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class RunVideos 7 | { 8 | public string Text { get; private set; } 9 | public ReadOnlyCollection Links { get; private set; } 10 | 11 | private RunVideos() { } 12 | 13 | private static Uri parseVideoLink(dynamic element) 14 | { 15 | string videoUri = element.uri as string; 16 | if (!string.IsNullOrEmpty(videoUri)) 17 | { 18 | if (!videoUri.StartsWith("http")) 19 | { 20 | videoUri = "http://" + videoUri; 21 | } 22 | 23 | if (Uri.IsWellFormedUriString(videoUri, UriKind.Absolute)) 24 | { 25 | return new Uri(videoUri); 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | public static RunVideos Parse(SpeedrunComClient client, dynamic videosElement) 33 | { 34 | if (videosElement == null) 35 | { 36 | return null; 37 | } 38 | 39 | var videos = new RunVideos 40 | { 41 | Text = videosElement.text as string, 42 | 43 | Links = client.ParseCollection(videosElement.links, new Func(parseVideoLink)) 44 | }; 45 | 46 | return videos; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class RunsClient 10 | { 11 | public const string Name = "runs"; 12 | 13 | private readonly SpeedrunComClient baseClient; 14 | 15 | public RunsClient(SpeedrunComClient baseClient) 16 | { 17 | this.baseClient = baseClient; 18 | } 19 | 20 | public static Uri GetRunsUri(string subUri) 21 | { 22 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 23 | } 24 | 25 | /// 26 | /// Fetch a Run object identified by its URI. 27 | /// 28 | /// The site URI for the run. 29 | /// Optional. If included, will dictate the embedded resources included in the response. 30 | /// 31 | public Run GetRunFromSiteUri(string siteUri, RunEmbeds embeds = default) 32 | { 33 | string id = GetRunIDFromSiteUri(siteUri); 34 | 35 | if (string.IsNullOrEmpty(id)) 36 | { 37 | return null; 38 | } 39 | 40 | return GetRun(id, embeds); 41 | } 42 | 43 | /// 44 | /// Fetch a Run ID identified by its URI. 45 | /// 46 | /// The site URI for the run. 47 | /// 48 | public string GetRunIDFromSiteUri(string siteUri) 49 | { 50 | try 51 | { 52 | Match match = Regex.Match(siteUri, "^(?:(?:https?://)?(?:www\\.)?speedrun\\.com/(?:\\w+/)?runs?/)?(\\w+)$"); 53 | 54 | return match.Groups[1].Value; 55 | } 56 | catch 57 | { 58 | return null; 59 | } 60 | } 61 | 62 | /// 63 | /// Fetch a Collection of Run objects identified by the parameters provided. 64 | /// 65 | /// Optional. If included, will filter runs by the user ID of the runner(s). 66 | /// Optional. If included, will filter runs by the name of the guest runner(s). 67 | /// Optional. If included, will filter runs by the user ID of the examiner. 68 | /// Optional. If included, will filter runs by the ID of the corresponding game. 69 | /// Optional. If included, will filter runs by the ID of the corresponding level. 70 | /// Optional. If included, will filter runs by the ID of the corresponding category 71 | /// Optional. If included, will filter runs by their platform. 72 | /// Optional. If included, will filter runs by their region. 73 | /// Optional. If included, will filter runs by their use of emulator. 74 | /// Optional. If included, will filter runs by their verification status. 75 | /// Optional. If included, will dictate the amount of elements included in each pagination. 76 | /// Optional. If included, will dictate the additional resources embedded in the response. 77 | /// Optional. If omitted, runs will be in the same order as the API. 78 | /// 79 | public IEnumerable GetRuns( 80 | string userId = null, string guestName = null, 81 | string examerUserId = null, string gameId = null, 82 | string levelId = null, string categoryId = null, 83 | string platformId = null, string regionId = null, 84 | bool onlyEmulatedRuns = false, RunStatusType? status = null, 85 | int? elementsPerPage = null, 86 | RunEmbeds embeds = default, 87 | RunsOrdering orderBy = default) 88 | { 89 | var parameters = new List() { embeds.ToString() }; 90 | 91 | if (!string.IsNullOrEmpty(userId)) 92 | { 93 | parameters.Add(string.Format("user={0}", Uri.EscapeDataString(userId))); 94 | } 95 | 96 | if (!string.IsNullOrEmpty(guestName)) 97 | { 98 | parameters.Add(string.Format("guest={0}", Uri.EscapeDataString(guestName))); 99 | } 100 | 101 | if (!string.IsNullOrEmpty(examerUserId)) 102 | { 103 | parameters.Add(string.Format("examiner={0}", Uri.EscapeDataString(examerUserId))); 104 | } 105 | 106 | if (!string.IsNullOrEmpty(gameId)) 107 | { 108 | parameters.Add(string.Format("game={0}", Uri.EscapeDataString(gameId))); 109 | } 110 | 111 | if (!string.IsNullOrEmpty(levelId)) 112 | { 113 | parameters.Add(string.Format("level={0}", Uri.EscapeDataString(levelId))); 114 | } 115 | 116 | if (!string.IsNullOrEmpty(categoryId)) 117 | { 118 | parameters.Add(string.Format("category={0}", Uri.EscapeDataString(categoryId))); 119 | } 120 | 121 | if (!string.IsNullOrEmpty(platformId)) 122 | { 123 | parameters.Add(string.Format("platform={0}", Uri.EscapeDataString(platformId))); 124 | } 125 | 126 | if (!string.IsNullOrEmpty(regionId)) 127 | { 128 | parameters.Add(string.Format("region={0}", Uri.EscapeDataString(regionId))); 129 | } 130 | 131 | if (onlyEmulatedRuns) 132 | { 133 | parameters.Add("emulated=yes"); 134 | } 135 | 136 | if (status.HasValue) 137 | { 138 | switch (status.Value) 139 | { 140 | case RunStatusType.New: 141 | parameters.Add("status=new"); break; 142 | case RunStatusType.Rejected: 143 | parameters.Add("status=rejected"); break; 144 | case RunStatusType.Verified: 145 | parameters.Add("status=verified"); break; 146 | } 147 | } 148 | 149 | if (elementsPerPage.HasValue) 150 | { 151 | parameters.Add(string.Format("max={0}", elementsPerPage)); 152 | } 153 | 154 | parameters.AddRange(orderBy.ToParameters()); 155 | 156 | Uri uri = GetRunsUri(parameters.ToParameters()); 157 | return baseClient.DoPaginatedRequest(uri, 158 | x => Run.Parse(baseClient, x) as Run); 159 | } 160 | 161 | /// 162 | /// Fetch a Run object identified by its ID. 163 | /// 164 | /// The ID of the run. 165 | /// Optional. If included, will dictate the additional resources embedded in the response. 166 | /// 167 | public Run GetRun(string runId, 168 | RunEmbeds embeds = default) 169 | { 170 | var parameters = new List() { embeds.ToString() }; 171 | 172 | Uri uri = GetRunsUri(string.Format("/{0}{1}", 173 | Uri.EscapeDataString(runId), 174 | parameters.ToParameters())); 175 | 176 | dynamic result = baseClient.DoRequest(uri); 177 | 178 | return Run.Parse(baseClient, result.data); 179 | } 180 | 181 | /// 182 | /// Posts a Run object to Speedrun.com. Authentication is required for this action. 183 | /// 184 | /// The ID of the category. 185 | /// The ID of the platform. 186 | /// Optional. If included, dictates the ID of the level. 187 | /// Optional. If included, dictates the date of the run. 188 | /// Optional. If included, dictates the ID of the region. 189 | /// Optional. If included, dictates real time. 190 | /// Optional. If included, dictates Real Time without loads. 191 | /// Optional. If included, dictates in game time. 192 | /// Optional. If included, dictates whether the run was performed on emulator. 193 | /// Optional. If included, dictates the URI of the video. 194 | /// Optional. If included, dictates the comment of the run. 195 | /// Optional. If included, dictates the variable values for the run. 196 | /// Optional. If included, dictates whether the run is verified automatically upon submitting. 197 | /// Optional. If included, dictates whether the run submission process is simulated. 198 | /// 199 | public Run Submit(string categoryId, 200 | string platformId, 201 | string levelId = null, 202 | DateTime? date = null, 203 | string regionId = null, 204 | TimeSpan? realTime = null, 205 | TimeSpan? realTimeWithoutLoads = null, 206 | TimeSpan? gameTime = null, 207 | bool? emulated = null, 208 | Uri videoUri = null, 209 | string comment = null, 210 | IEnumerable variables = null, 211 | bool? verify = null, 212 | bool simulateSubmitting = false) 213 | { 214 | var parameters = new List(); 215 | 216 | if (simulateSubmitting) 217 | { 218 | parameters.Add("dry=yes"); 219 | } 220 | 221 | Uri uri = GetRunsUri(parameters.ToParameters()); 222 | 223 | dynamic postBody = new DynamicJsonObject(); 224 | dynamic runElement = new DynamicJsonObject(); 225 | 226 | runElement.category = categoryId; 227 | runElement.platform = platformId; 228 | 229 | if (!string.IsNullOrEmpty(levelId)) 230 | { 231 | runElement.level = levelId; 232 | } 233 | 234 | if (date.HasValue) 235 | { 236 | runElement.date = date.Value.ToUniversalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); 237 | } 238 | 239 | if (!string.IsNullOrEmpty(regionId)) 240 | { 241 | runElement.region = regionId; 242 | } 243 | 244 | if (verify.HasValue) 245 | { 246 | runElement.verified = verify; 247 | } 248 | 249 | dynamic timesElement = new DynamicJsonObject(); 250 | 251 | if (!realTime.HasValue 252 | && !realTimeWithoutLoads.HasValue 253 | && !gameTime.HasValue) 254 | { 255 | throw new APIException("You need to provide at least one time."); 256 | } 257 | 258 | if (realTime.HasValue) 259 | { 260 | timesElement.realtime = realTime.Value.TotalSeconds; 261 | } 262 | 263 | if (realTimeWithoutLoads.HasValue) 264 | { 265 | timesElement.realtime_noloads = realTimeWithoutLoads.Value.TotalSeconds; 266 | } 267 | 268 | if (gameTime.HasValue) 269 | { 270 | timesElement.ingame = gameTime.Value.TotalSeconds; 271 | } 272 | 273 | runElement.times = timesElement; 274 | 275 | if (emulated.HasValue) 276 | { 277 | runElement.emulated = emulated.Value; 278 | } 279 | 280 | if (videoUri != null) 281 | { 282 | runElement.video = videoUri.AbsoluteUri; 283 | } 284 | 285 | if (!string.IsNullOrEmpty(comment)) 286 | { 287 | runElement.comment = comment; 288 | } 289 | 290 | if (variables != null) 291 | { 292 | var variablesList = variables.ToList(); 293 | 294 | if (variablesList.Any()) 295 | { 296 | var variablesElement = new Dictionary(); 297 | 298 | foreach (VariableValue variable in variablesList) 299 | { 300 | string key = variable.VariableID; 301 | dynamic value = new DynamicJsonObject(); 302 | 303 | if (variable.IsCustomValue) 304 | { 305 | value.type = "user-defined"; 306 | value.value = variable.Value; 307 | } 308 | else 309 | { 310 | value.type = "pre-defined"; 311 | value.value = variable.ID; 312 | } 313 | 314 | variablesElement.Add(key, value); 315 | } 316 | 317 | runElement.variables = variablesElement; 318 | } 319 | } 320 | 321 | postBody.run = runElement; 322 | 323 | dynamic result = baseClient.DoPostRequest(uri, postBody.ToString()); 324 | 325 | return Run.Parse(baseClient, result.data); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Runs/RunsOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Runs in responses. 7 | /// 8 | public enum RunsOrdering : int 9 | { 10 | Game = 0, 11 | GameDescending, 12 | Category, 13 | CategoryDescending, 14 | Level, 15 | LevelDescending, 16 | Platform, 17 | PlatformDescending, 18 | Region, 19 | RegionDescending, 20 | Emulated, 21 | EmulatedDescending, 22 | Date, 23 | DateDescending, 24 | DateSubmitted, 25 | DateSubmittedDescending, 26 | Status, 27 | StatusDescending, 28 | VerifyDate, 29 | VerifyDateDescending 30 | } 31 | 32 | internal static class RunsOrderingHelpers 33 | { 34 | internal static IEnumerable ToParameters(this RunsOrdering ordering) 35 | { 36 | bool isDescending = ((int)ordering & 1) == 1; 37 | if (isDescending) 38 | { 39 | ordering = (RunsOrdering)((int)ordering - 1); 40 | } 41 | 42 | string str = ""; 43 | 44 | switch (ordering) 45 | { 46 | case RunsOrdering.Category: 47 | str = "category"; break; 48 | case RunsOrdering.Level: 49 | str = "level"; break; 50 | case RunsOrdering.Platform: 51 | str = "platform"; break; 52 | case RunsOrdering.Region: 53 | str = "region"; break; 54 | case RunsOrdering.Emulated: 55 | str = "emulated"; break; 56 | case RunsOrdering.Date: 57 | str = "date"; break; 58 | case RunsOrdering.DateSubmitted: 59 | str = "submitted"; break; 60 | case RunsOrdering.Status: 61 | str = "status"; break; 62 | case RunsOrdering.VerifyDate: 63 | str = "verify-date"; break; 64 | } 65 | 66 | var list = new List(); 67 | 68 | if (!string.IsNullOrEmpty(str)) 69 | { 70 | list.Add(string.Format("orderby={0}", str)); 71 | } 72 | 73 | if (isDescending) 74 | { 75 | list.Add("direction=desc"); 76 | } 77 | 78 | return list; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Series/Series.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class Series : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public string Name { get; private set; } 13 | public string JapaneseName { get; private set; } 14 | public string Abbreviation { get; private set; } 15 | public Uri WebLink { get; private set; } 16 | public DateTime? CreationDate { get; private set; } 17 | public Assets Assets { get; private set; } 18 | 19 | #region Embeds 20 | 21 | private Lazy> moderatorUsers; 22 | 23 | /// 24 | /// null when embedded 25 | /// 26 | public ReadOnlyCollection Moderators { get; private set; } 27 | 28 | public ReadOnlyCollection ModeratorUsers => moderatorUsers.Value; 29 | 30 | #endregion 31 | 32 | #region Links 33 | 34 | public IEnumerable Games { get; private set; } 35 | 36 | #endregion 37 | 38 | private Series() { } 39 | 40 | public static Series Parse(SpeedrunComClient client, dynamic seriesElement) 41 | { 42 | var series = new Series 43 | { 44 | //Parse Attributes 45 | 46 | ID = seriesElement.id as string, 47 | Name = seriesElement.names.international as string, 48 | JapaneseName = seriesElement.names.japanese as string, 49 | WebLink = new Uri(seriesElement.weblink as string), 50 | Abbreviation = seriesElement.abbreviation as string 51 | }; 52 | 53 | string created = seriesElement.created as string; 54 | if (!string.IsNullOrEmpty(created)) 55 | { 56 | series.CreationDate = DateTime.Parse(created, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 57 | } 58 | 59 | series.Assets = Assets.Parse(client, seriesElement.assets); 60 | 61 | //Parse Embeds 62 | 63 | if (seriesElement.moderators is DynamicJsonObject && seriesElement.moderators.Properties.ContainsKey("data")) 64 | { 65 | User userParser(dynamic x) 66 | { 67 | return User.Parse(client, x) as User; 68 | } 69 | 70 | ReadOnlyCollection users = client.ParseCollection(seriesElement.moderators.data, (Func)userParser); 71 | series.moderatorUsers = new Lazy>(() => users); 72 | } 73 | else if (seriesElement.moderators is DynamicJsonObject) 74 | { 75 | var moderatorsProperties = seriesElement.moderators.Properties as IDictionary; 76 | series.Moderators = moderatorsProperties.Select(x => Moderator.Parse(client, x)).ToList().AsReadOnly(); 77 | 78 | series.moderatorUsers = new Lazy>( 79 | () => 80 | { 81 | ReadOnlyCollection users; 82 | 83 | if (series.Moderators.Count(x => !x.user.IsValueCreated) > 1) 84 | { 85 | users = client.Games.GetGame(series.ID, embeds: new GameEmbeds(embedModerators: true)).ModeratorUsers; 86 | 87 | foreach (User user in users) 88 | { 89 | Moderator moderator = series.Moderators.FirstOrDefault(x => x.UserID == user.ID); 90 | if (moderator != null) 91 | { 92 | moderator.user = new Lazy(() => user); 93 | } 94 | } 95 | } 96 | else 97 | { 98 | users = series.Moderators.Select(x => x.User).ToList().AsReadOnly(); 99 | } 100 | 101 | return users; 102 | }); 103 | } 104 | else 105 | { 106 | series.Moderators = new ReadOnlyCollection(new Moderator[0]); 107 | series.moderatorUsers = new Lazy>(() => new List().AsReadOnly()); 108 | } 109 | 110 | //Parse Links 111 | 112 | series.Games = client.Series.GetGames(series.ID).Select(game => 113 | { 114 | game.series = new Lazy(() => series); 115 | return game; 116 | }).Cache(); 117 | 118 | return series; 119 | } 120 | 121 | public override int GetHashCode() 122 | { 123 | return (ID ?? string.Empty).GetHashCode(); 124 | } 125 | 126 | public override bool Equals(object obj) 127 | { 128 | if (obj is not Series other) 129 | { 130 | return false; 131 | } 132 | 133 | return ID == other.ID; 134 | } 135 | 136 | public override string ToString() 137 | { 138 | return Name; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Series/SeriesClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SpeedrunComSharp; 5 | 6 | public class SeriesClient 7 | { 8 | public const string Name = "series"; 9 | 10 | private readonly SpeedrunComClient baseClient; 11 | 12 | public SeriesClient(SpeedrunComClient baseClient) 13 | { 14 | this.baseClient = baseClient; 15 | } 16 | 17 | public static Uri GetSeriesUri(string subUri) 18 | { 19 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 20 | } 21 | 22 | /// 23 | /// Fetch a Series object identified by its URI. 24 | /// 25 | /// The site URI for the series. 26 | /// Optional. If included, will dictate the embedded resources included in the response. 27 | /// 28 | public Series GetSeriesFromSiteUri(string siteUri, SeriesEmbeds embeds = default) 29 | { 30 | string id = GetSeriesIDFromSiteUri(siteUri); 31 | 32 | if (string.IsNullOrEmpty(id)) 33 | { 34 | return null; 35 | } 36 | 37 | return GetSingleSeries(id, embeds); 38 | } 39 | 40 | /// 41 | /// Fetch a Series ID identified by its URI. 42 | /// 43 | /// The site URI for the series. 44 | /// 45 | public string GetSeriesIDFromSiteUri(string siteUri) 46 | { 47 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 48 | 49 | if (elementDescription == null 50 | || elementDescription.Type != ElementType.Series) 51 | { 52 | return null; 53 | } 54 | 55 | return elementDescription.ID; 56 | } 57 | 58 | /// 59 | /// Fetch a Collection of Series objects identified by the parameters provided. 60 | /// 61 | /// Optional. If included, will filter series by their name. 62 | /// Optional. If included, will filter series by their abbreviation. 63 | /// Optional. If included, will filter series by their moderators. 64 | /// Optional. If included, will dictate the amount of elements included in each pagination. 65 | /// Optional. If included, will dictate the additional resources embedded in the response. 66 | /// Optional. If omitted, series will be in the same order as the API. 67 | /// 68 | public IEnumerable GetMultipleSeries( 69 | string name = null, string abbreviation = null, 70 | string moderatorId = null, int? elementsPerPage = null, 71 | SeriesEmbeds embeds = default, 72 | SeriesOrdering orderBy = default) 73 | { 74 | var parameters = new List() { embeds.ToString() }; 75 | 76 | parameters.AddRange(orderBy.ToParameters()); 77 | 78 | if (!string.IsNullOrEmpty(name)) 79 | { 80 | parameters.Add(string.Format("name={0}", Uri.EscapeDataString(name))); 81 | } 82 | 83 | if (!string.IsNullOrEmpty(abbreviation)) 84 | { 85 | parameters.Add(string.Format("abbreviation={0}", Uri.EscapeDataString(abbreviation))); 86 | } 87 | 88 | if (!string.IsNullOrEmpty(moderatorId)) 89 | { 90 | parameters.Add(string.Format("moderator={0}", Uri.EscapeDataString(moderatorId))); 91 | } 92 | 93 | if (elementsPerPage.HasValue) 94 | { 95 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 96 | } 97 | 98 | Uri uri = GetSeriesUri(parameters.ToParameters()); 99 | return baseClient.DoPaginatedRequest(uri, 100 | x => Series.Parse(baseClient, x) as Series); 101 | } 102 | 103 | /// 104 | /// Fetch a Series object identified by its ID. 105 | /// 106 | /// The ID of the series. 107 | /// Optional. If included, will dictate the additional resources embedded in the response. 108 | /// 109 | public Series GetSingleSeries(string seriesId, SeriesEmbeds embeds = default) 110 | { 111 | var parameters = new List() { embeds.ToString() }; 112 | 113 | Uri uri = GetSeriesUri(string.Format("/{0}{1}", 114 | Uri.EscapeDataString(seriesId), 115 | parameters.ToParameters())); 116 | 117 | dynamic result = baseClient.DoRequest(uri); 118 | 119 | return Series.Parse(baseClient, result.data); 120 | } 121 | 122 | /// 123 | /// 124 | /// 125 | /// The ID of the series. 126 | /// Optional. If included, will filter series by their name. 127 | /// Optional. If included, will filter series by their release year. 128 | /// Optional. If included, will filter series by their platform. 129 | /// Optional. If included, will filter series by their region. 130 | /// Optional. If included, will filter series by their moderators. 131 | /// Optional. If included, will dictate the amount of elements included in each pagination. 132 | /// Optional. If included, will dictate the additional resources embedded in the response. 133 | /// Optional. If omitted, series will be in the same order as the API. 134 | /// 135 | public IEnumerable GetGames( 136 | string seriesId, 137 | string name = null, int? yearOfRelease = null, 138 | string platformId = null, string regionId = null, 139 | string moderatorId = null, int? elementsPerPage = null, 140 | GameEmbeds embeds = default, 141 | GamesOrdering orderBy = default) 142 | { 143 | var parameters = new List() { embeds.ToString() }; 144 | 145 | parameters.AddRange(orderBy.ToParameters()); 146 | 147 | if (!string.IsNullOrEmpty(name)) 148 | { 149 | parameters.Add(string.Format("name={0}", Uri.EscapeDataString(name))); 150 | } 151 | 152 | if (yearOfRelease.HasValue) 153 | { 154 | parameters.Add(string.Format("released={0}", yearOfRelease.Value)); 155 | } 156 | 157 | if (!string.IsNullOrEmpty(platformId)) 158 | { 159 | parameters.Add(string.Format("platform={0}", Uri.EscapeDataString(platformId))); 160 | } 161 | 162 | if (!string.IsNullOrEmpty(regionId)) 163 | { 164 | parameters.Add(string.Format("region={0}", Uri.EscapeDataString(regionId))); 165 | } 166 | 167 | if (!string.IsNullOrEmpty(moderatorId)) 168 | { 169 | parameters.Add(string.Format("moderator={0}", Uri.EscapeDataString(moderatorId))); 170 | } 171 | 172 | if (elementsPerPage.HasValue) 173 | { 174 | parameters.Add(string.Format("max={0}", elementsPerPage.Value)); 175 | } 176 | 177 | Uri uri = GetSeriesUri(string.Format("/{0}/games{1}", 178 | Uri.EscapeDataString(seriesId), 179 | parameters.ToParameters())); 180 | 181 | return baseClient.DoPaginatedRequest(uri, 182 | x => Game.Parse(baseClient, x) as Game); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Series/SeriesEmbeds.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public struct SeriesEmbeds 4 | { 5 | private Embeds embeds; 6 | 7 | public bool EmbedModerators 8 | { 9 | get => embeds["moderators"]; 10 | set => embeds["moderators"] = value; 11 | } 12 | 13 | /// 14 | /// Options for embedding resources in Series responses. 15 | /// 16 | /// Dictates whether a Collection of User objects containing each moderator is included in the response. 17 | public SeriesEmbeds( 18 | bool embedModerators = false) 19 | { 20 | embeds = new Embeds(); 21 | EmbedModerators = embedModerators; 22 | } 23 | 24 | public override string ToString() 25 | { 26 | return embeds.ToString(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Series/SeriesOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Series in responses. 7 | /// 8 | public enum SeriesOrdering : int 9 | { 10 | Name = 0, 11 | NameDescending, 12 | JapaneseName, 13 | JapaneseNameDescending, 14 | Abbreviation, 15 | AbbreviationDescending, 16 | CreationDate, 17 | CreationDateDescending 18 | } 19 | 20 | internal static class SeriesOrderingHelpers 21 | { 22 | internal static IEnumerable ToParameters(this SeriesOrdering ordering) 23 | { 24 | bool isDescending = ((int)ordering & 1) == 1; 25 | if (isDescending) 26 | { 27 | ordering = (SeriesOrdering)((int)ordering - 1); 28 | } 29 | 30 | string str = ""; 31 | 32 | switch (ordering) 33 | { 34 | case SeriesOrdering.JapaneseName: 35 | str = "name.jap"; break; 36 | case SeriesOrdering.Abbreviation: 37 | str = "abbreviation"; break; 38 | case SeriesOrdering.CreationDate: 39 | str = "created"; break; 40 | } 41 | 42 | var list = new List(); 43 | 44 | if (!string.IsNullOrEmpty(str)) 45 | { 46 | list.Add(string.Format("orderby={0}", str)); 47 | } 48 | 49 | if (isDescending) 50 | { 51 | list.Add("direction=desc"); 52 | } 53 | 54 | return list; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/SpeedrunComClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | 9 | namespace SpeedrunComSharp; 10 | 11 | public class SpeedrunComClient 12 | { 13 | public static readonly Uri BaseUri = new("https://www.speedrun.com/"); 14 | public static readonly Uri APIUri = new(BaseUri, "api/v1/"); 15 | public const string APIHttpHeaderRelation = "alternate https://www.speedrun.com/api"; 16 | 17 | public string AccessToken { internal get; set; } 18 | 19 | public bool IsAccessTokenValid 20 | { 21 | get 22 | { 23 | if (AccessToken == null) 24 | { 25 | return false; 26 | } 27 | 28 | try 29 | { 30 | User profile = Profile; 31 | return true; 32 | } 33 | catch { } 34 | 35 | return false; 36 | } 37 | } 38 | 39 | public string UserAgent { get; private set; } 40 | private Dictionary Cache { get; set; } 41 | public int MaxCacheElements { get; private set; } 42 | 43 | public TimeSpan Timeout { get; private set; } 44 | 45 | /// 46 | /// Methods for interacting with Categories. 47 | /// 48 | public CategoriesClient Categories { get; private set; } 49 | /// 50 | /// Methods for interacting with Games. 51 | /// 52 | public GamesClient Games { get; private set; } 53 | /// 54 | /// Methods for interacting with Guest users. 55 | /// 56 | public GuestsClient Guests { get; private set; } 57 | /// 58 | /// Methods for interacting with Leaderboards. 59 | /// 60 | public LeaderboardsClient Leaderboards { get; private set; } 61 | /// 62 | /// Methods for interacting with Levels. 63 | /// 64 | public LevelsClient Levels { get; private set; } 65 | /// 66 | /// Methods for interacting with Notifications. 67 | /// 68 | public NotificationsClient Notifications { get; private set; } 69 | /// 70 | /// Methods for interacting with Platforms. 71 | /// 72 | public PlatformsClient Platforms { get; private set; } 73 | /// 74 | /// Methods for interacting with Regions. 75 | /// 76 | public RegionsClient Regions { get; private set; } 77 | /// 78 | /// Methods for interacting with Runs. 79 | /// 80 | public RunsClient Runs { get; private set; } 81 | /// 82 | /// Methods for interacting with Series. 83 | /// 84 | public SeriesClient Series { get; private set; } 85 | /// 86 | /// Methods for interacting with Users. 87 | /// 88 | public UsersClient Users { get; private set; } 89 | /// 90 | /// Methods for interacting with Variables. 91 | /// 92 | public VariablesClient Variables { get; private set; } 93 | 94 | public User Profile 95 | { 96 | get 97 | { 98 | Uri uri = GetProfileUri(string.Empty); 99 | dynamic result = DoRequest(uri); 100 | return User.Parse(this, result.data); 101 | } 102 | } 103 | 104 | public SpeedrunComClient(string userAgent = "SpeedRunComSharp/1.0", 105 | string accessToken = null, int maxCacheElements = 50, 106 | TimeSpan? timeout = null) 107 | { 108 | Timeout = timeout ?? TimeSpan.FromSeconds(30); 109 | 110 | UserAgent = userAgent; 111 | MaxCacheElements = maxCacheElements; 112 | AccessToken = accessToken; 113 | Cache = []; 114 | Categories = new CategoriesClient(this); 115 | Games = new GamesClient(this); 116 | Guests = new GuestsClient(this); 117 | Leaderboards = new LeaderboardsClient(this); 118 | Levels = new LevelsClient(this); 119 | Notifications = new NotificationsClient(this); 120 | Platforms = new PlatformsClient(this); 121 | Regions = new RegionsClient(this); 122 | Runs = new RunsClient(this); 123 | Series = new SeriesClient(this); 124 | Users = new UsersClient(this); 125 | Variables = new VariablesClient(this); 126 | } 127 | 128 | public static Uri GetSiteUri(string subUri) 129 | { 130 | return new Uri(BaseUri, subUri); 131 | } 132 | 133 | public static Uri GetAPIUri(string subUri) 134 | { 135 | return new Uri(APIUri, subUri); 136 | } 137 | 138 | public static Uri GetProfileUri(string subUri) 139 | { 140 | return GetAPIUri(string.Format("profile{0}", subUri)); 141 | } 142 | 143 | public ElementDescription GetElementDescriptionFromSiteUri(string siteUri) 144 | { 145 | try 146 | { 147 | var request = WebRequest.Create(siteUri); 148 | request.Timeout = (int)Timeout.TotalMilliseconds; 149 | WebResponse response = request.GetResponse(); 150 | string linksString = response.Headers["Link"]; 151 | ReadOnlyCollection links = HttpWebLink.ParseLinks(linksString); 152 | HttpWebLink link = links.FirstOrDefault(x => x.Relation == APIHttpHeaderRelation); 153 | 154 | if (link == null) 155 | { 156 | return null; 157 | } 158 | 159 | string uri = link.Uri; 160 | var elementDescription = ElementDescription.ParseUri(uri); 161 | 162 | return elementDescription; 163 | } 164 | catch 165 | { 166 | return null; 167 | } 168 | } 169 | 170 | internal ReadOnlyCollection ParseCollection(dynamic collection, Func parser) 171 | { 172 | if (collection is not IEnumerable enumerable) 173 | { 174 | return new List(new T[0]).AsReadOnly(); 175 | } 176 | 177 | return enumerable.Select(parser).ToList().AsReadOnly(); 178 | } 179 | 180 | internal ReadOnlyCollection ParseCollection(dynamic collection) 181 | { 182 | if (collection is not IEnumerable enumerable) 183 | { 184 | return new List(new T[0]).AsReadOnly(); 185 | } 186 | 187 | return enumerable.OfType().ToList().AsReadOnly(); 188 | } 189 | 190 | internal APIException ParseException(Stream stream) 191 | { 192 | dynamic json = JSON.FromStream(stream); 193 | var properties = json.Properties as IDictionary; 194 | if (properties.ContainsKey("errors")) 195 | { 196 | var errors = json.errors as IList; 197 | return new APIException(json.message as string, errors.Select(x => x as string)); 198 | } 199 | else 200 | { 201 | return new APIException(json.message as string); 202 | } 203 | } 204 | 205 | internal dynamic DoPostRequest(Uri uri, string postBody) 206 | { 207 | try 208 | { 209 | return JSON.FromUriPost(uri, UserAgent, AccessToken, Timeout, postBody); 210 | } 211 | catch (WebException ex) 212 | { 213 | try 214 | { 215 | using Stream stream = ex.Response.GetResponseStream(); 216 | throw ParseException(stream); 217 | } 218 | catch (APIException ex2) 219 | { 220 | throw ex2; 221 | } 222 | catch 223 | { 224 | throw ex; 225 | } 226 | } 227 | } 228 | 229 | internal dynamic DoRequest(Uri uri) 230 | { 231 | lock (this) 232 | { 233 | dynamic result; 234 | 235 | if (Cache.ContainsKey(uri)) 236 | { 237 | #if DEBUG 238 | Debug.WriteLine($"Cached API Call: {uri.AbsoluteUri}"); 239 | #endif 240 | result = Cache[uri]; 241 | Cache.Remove(uri); 242 | } 243 | else 244 | { 245 | #if DEBUG 246 | Debug.WriteLine($"Uncached API Call: {uri.AbsoluteUri}"); 247 | #endif 248 | try 249 | { 250 | result = JSON.FromUri(uri, UserAgent, AccessToken, Timeout); 251 | } 252 | catch (WebException ex) 253 | { 254 | try 255 | { 256 | using Stream stream = ex.Response.GetResponseStream(); 257 | throw ParseException(stream); 258 | } 259 | catch (APIException ex2) 260 | { 261 | throw ex2; 262 | } 263 | catch 264 | { 265 | throw ex; 266 | } 267 | } 268 | } 269 | 270 | Cache.Add(uri, result); 271 | 272 | while (Cache.Count > MaxCacheElements) 273 | { 274 | Cache.Remove(Cache.Keys.First()); 275 | } 276 | 277 | return result; 278 | } 279 | } 280 | 281 | internal ReadOnlyCollection DoDataCollectionRequest(Uri uri, Func parser) 282 | { 283 | dynamic result = DoRequest(uri); 284 | if (result.data is not IEnumerable elements) 285 | { 286 | return new ReadOnlyCollection(new T[0]); 287 | } 288 | 289 | return elements.Select(parser).ToList().AsReadOnly(); 290 | } 291 | 292 | private IEnumerable doPaginatedRequest(Uri uri, Func parser) 293 | { 294 | do 295 | { 296 | dynamic result = DoRequest(uri); 297 | 298 | if (result.pagination.size == 0) 299 | { 300 | yield break; 301 | } 302 | 303 | var elements = result.data as IEnumerable; 304 | 305 | foreach (dynamic element in elements) 306 | { 307 | yield return parser(element); 308 | } 309 | 310 | var links = result.pagination.links as IEnumerable; 311 | if (links == null) 312 | { 313 | yield break; 314 | } 315 | 316 | dynamic paginationLink = links.FirstOrDefault(x => x.rel == "next"); 317 | if (paginationLink == null) 318 | { 319 | yield break; 320 | } 321 | 322 | uri = new Uri(paginationLink.uri as string); 323 | } while (true); 324 | } 325 | 326 | internal IEnumerable DoPaginatedRequest(Uri uri, Func parser) 327 | { 328 | return doPaginatedRequest(uri, parser).Cache(); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/SpeedrunComSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net4.8.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/StringHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | internal static class StringHelpers 8 | { 9 | internal static string ToParameters(this string parameters) 10 | { 11 | if (string.IsNullOrEmpty(parameters)) 12 | { 13 | return ""; 14 | } 15 | else 16 | { 17 | return "?" + parameters; 18 | } 19 | } 20 | 21 | internal static string ToParameters(this IEnumerable parameters) 22 | { 23 | var list = parameters.Where(x => !string.IsNullOrEmpty(x)).ToList(); 24 | if (list.Any()) 25 | { 26 | return "?" + list.Aggregate("&"); 27 | } 28 | else 29 | { 30 | return ""; 31 | } 32 | } 33 | 34 | internal static string Aggregate(this IEnumerable list, string combiner) 35 | { 36 | var builder = new StringBuilder(); 37 | 38 | foreach (string element in list) 39 | { 40 | builder.Append(element); 41 | builder.Append(combiner); 42 | } 43 | 44 | builder.Length -= combiner.Length; 45 | 46 | return builder.ToString(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/Country.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public class Country 4 | { 5 | public string Code { get; private set; } 6 | public string Name { get; private set; } 7 | public string JapaneseName { get; private set; } 8 | 9 | private Country() { } 10 | 11 | public static Country Parse(SpeedrunComClient client, dynamic countryElement) 12 | { 13 | var country = new Country 14 | { 15 | Code = countryElement.code as string, 16 | Name = countryElement.names.international as string, 17 | JapaneseName = countryElement.names.japanese as string 18 | }; 19 | 20 | return country; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return Name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/CountryRegion.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public class CountryRegion 4 | { 5 | public string Code { get; private set; } 6 | public string Name { get; private set; } 7 | public string JapaneseName { get; private set; } 8 | 9 | private CountryRegion() { } 10 | 11 | public static CountryRegion Parse(SpeedrunComClient client, dynamic regionElement) 12 | { 13 | var region = new CountryRegion 14 | { 15 | Code = regionElement.code as string, 16 | Name = regionElement.names.international as string, 17 | JapaneseName = regionElement.names.japanese as string 18 | }; 19 | 20 | return region; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return Name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/Location.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public class Location 4 | { 5 | public Country Country { get; private set; } 6 | public CountryRegion Region { get; private set; } 7 | 8 | private Location() { } 9 | 10 | public static Location Parse(SpeedrunComClient client, dynamic locationElement) 11 | { 12 | var location = new Location(); 13 | 14 | if (locationElement != null) 15 | { 16 | location.Country = Country.Parse(client, locationElement.country) as Country; 17 | 18 | if (locationElement.region != null) 19 | { 20 | location.Region = CountryRegion.Parse(client, locationElement.region) as CountryRegion; 21 | } 22 | } 23 | 24 | return location; 25 | } 26 | 27 | public override string ToString() 28 | { 29 | if (Region == null) 30 | { 31 | return Country.Name; 32 | } 33 | else 34 | { 35 | return Country.Name + " " + Region.Name; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class User : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public string Name { get; private set; } 13 | public string JapaneseName { get; private set; } 14 | public string[] Pronouns { get; private set; } 15 | public Uri WebLink { get; private set; } 16 | public UserNameStyle NameStyle { get; private set; } 17 | public UserRole Role { get; private set; } 18 | public DateTime? SignUpDate { get; private set; } 19 | public Location Location { get; private set; } 20 | 21 | public Uri TwitchProfile { get; private set; } 22 | public Uri HitboxProfile { get; private set; } 23 | public Uri YoutubeProfile { get; private set; } 24 | public Uri TwitterProfile { get; private set; } 25 | public Uri SpeedRunsLiveProfile { get; private set; } 26 | 27 | public Uri Icon { get; private set; } 28 | public Uri Image { get; private set; } 29 | 30 | #region Links 31 | 32 | private Lazy> personalBests; 33 | 34 | public IEnumerable Runs { get; private set; } 35 | public IEnumerable ModeratedGames { get; private set; } 36 | public ReadOnlyCollection PersonalBests => personalBests.Value; 37 | 38 | #endregion 39 | 40 | private User() { } 41 | 42 | private static UserRole parseUserRole(string role) 43 | { 44 | return role switch 45 | { 46 | "banned" => UserRole.Banned, 47 | "user" => UserRole.User, 48 | "trusted" => UserRole.Trusted, 49 | "moderator" => UserRole.Moderator, 50 | "admin" => UserRole.Admin, 51 | "programmer" => UserRole.Programmer, 52 | "contentmoderator" => UserRole.ContentModerator, 53 | _ => throw new ArgumentException("role"), 54 | }; 55 | } 56 | 57 | public static User Parse(SpeedrunComClient client, dynamic userElement) 58 | { 59 | var user = new User(); 60 | 61 | var properties = userElement.Properties as IDictionary; 62 | 63 | //Parse Attributes 64 | 65 | user.ID = userElement.id as string; 66 | user.Name = userElement.names.international as string; 67 | user.JapaneseName = userElement.names.japanese as string; 68 | 69 | string pronounsTemp = userElement.pronouns as string; 70 | if (!string.IsNullOrWhiteSpace(pronounsTemp)) 71 | { 72 | user.Pronouns = pronounsTemp.Split(new string[] { ", " }, StringSplitOptions.None); 73 | } 74 | 75 | user.WebLink = new Uri(userElement.weblink as string); 76 | user.NameStyle = UserNameStyle.Parse(client, properties["name-style"]) as UserNameStyle; 77 | user.Role = parseUserRole(userElement.role as string); 78 | 79 | string signUpDate = userElement.signup as string; 80 | if (!string.IsNullOrEmpty(signUpDate)) 81 | { 82 | user.SignUpDate = DateTime.Parse(signUpDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); 83 | } 84 | 85 | user.Location = Location.Parse(client, userElement.location) as Location; 86 | 87 | dynamic twitchLink = userElement.twitch; 88 | if (twitchLink != null) 89 | { 90 | user.TwitchProfile = new Uri(twitchLink.uri as string); 91 | } 92 | 93 | dynamic hitboxLink = userElement.hitbox; 94 | if (hitboxLink != null) 95 | { 96 | user.HitboxProfile = new Uri(hitboxLink.uri as string); 97 | } 98 | 99 | dynamic youtubeLink = userElement.youtube; 100 | if (youtubeLink != null) 101 | { 102 | user.YoutubeProfile = new Uri(youtubeLink.uri as string); 103 | } 104 | 105 | dynamic twitterLink = userElement.twitter; 106 | if (twitterLink != null) 107 | { 108 | user.TwitterProfile = new Uri(twitterLink.uri as string); 109 | } 110 | 111 | dynamic speedRunsLiveLink = userElement.speedrunslive; 112 | if (speedRunsLiveLink != null) 113 | { 114 | user.SpeedRunsLiveProfile = new Uri(speedRunsLiveLink.uri as string); 115 | } 116 | 117 | dynamic iconTemp = userElement.assets.icon.uri; 118 | if (iconTemp != null) 119 | { 120 | user.Icon = new Uri(iconTemp as string); 121 | } 122 | 123 | dynamic imageTemp = userElement.assets.image.uri; 124 | if (imageTemp != null) 125 | { 126 | user.Image = new Uri(imageTemp as string); 127 | } 128 | 129 | //Parse Links 130 | 131 | user.Runs = client.Runs.GetRuns(userId: user.ID); 132 | user.ModeratedGames = client.Games.GetGames(moderatorId: user.ID); 133 | user.personalBests = new Lazy>(() => 134 | { 135 | ReadOnlyCollection records = client.Users.GetPersonalBests(userId: user.ID); 136 | var lazy = new Lazy(() => user); 137 | 138 | foreach (Record record in records) 139 | { 140 | Player player = record.Players.FirstOrDefault(x => x.UserID == user.ID); 141 | if (player != null) 142 | { 143 | player.user = lazy; 144 | } 145 | } 146 | 147 | return records; 148 | }); 149 | 150 | return user; 151 | } 152 | 153 | public override int GetHashCode() 154 | { 155 | return (ID ?? string.Empty).GetHashCode(); 156 | } 157 | 158 | public override bool Equals(object obj) 159 | { 160 | if (obj is not User other) 161 | { 162 | return false; 163 | } 164 | 165 | return ID == other.ID; 166 | } 167 | 168 | public override string ToString() 169 | { 170 | return Name; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/UserNameStyle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class UserNameStyle 6 | { 7 | public bool IsGradient { get; private set; } 8 | public string LightSolidColorCode { get; private set; } 9 | public string LightGradientStartColorCode 10 | { 11 | get => LightSolidColorCode; 12 | private set => LightSolidColorCode = value; 13 | } 14 | public string LightGradientEndColorCode { get; private set; } 15 | public string DarkSolidColorCode { get; private set; } 16 | public string DarkGradientStartColorCode 17 | { 18 | get => DarkSolidColorCode; 19 | private set => DarkSolidColorCode = value; 20 | } 21 | public string DarkGradientEndColorCode { get; private set; } 22 | 23 | private UserNameStyle() { } 24 | 25 | public static UserNameStyle Parse(SpeedrunComClient client, dynamic styleElement) 26 | { 27 | var style = new UserNameStyle 28 | { 29 | IsGradient = styleElement.style == "gradient" 30 | }; 31 | 32 | if (style.IsGradient) 33 | { 34 | var properties = styleElement.Properties as IDictionary; 35 | dynamic colorFrom = properties["color-from"]; 36 | dynamic colorTo = properties["color-to"]; 37 | 38 | style.LightGradientStartColorCode = colorFrom.light as string; 39 | style.LightGradientEndColorCode = colorTo.light as string; 40 | style.DarkGradientStartColorCode = colorFrom.dark as string; 41 | style.DarkGradientEndColorCode = colorTo.dark as string; 42 | } 43 | else 44 | { 45 | style.LightSolidColorCode = styleElement.color.light as string; 46 | style.DarkSolidColorCode = styleElement.color.dark as string; 47 | } 48 | 49 | return style; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/UserRole.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum UserRole 4 | { 5 | Banned, User, Trusted, Moderator, Admin, Programmer, ContentModerator 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/UsersClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class UsersClient 8 | { 9 | public const string Name = "users"; 10 | 11 | private readonly SpeedrunComClient baseClient; 12 | 13 | public UsersClient(SpeedrunComClient baseClient) 14 | { 15 | this.baseClient = baseClient; 16 | } 17 | 18 | public static Uri GetUsersUri(string subUri) 19 | { 20 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 21 | } 22 | 23 | /// 24 | /// Fetch a User object identified by its URI. 25 | /// 26 | /// The site URI for the user. 27 | /// 28 | public User GetUserFromSiteUri(string siteUri) 29 | { 30 | string id = GetUserIDFromSiteUri(siteUri); 31 | 32 | if (string.IsNullOrEmpty(id)) 33 | { 34 | return null; 35 | } 36 | 37 | return GetUser(id); 38 | } 39 | 40 | /// 41 | /// Fetch a User ID identified by its URI. 42 | /// 43 | /// The site URI for the user. 44 | /// 45 | public string GetUserIDFromSiteUri(string siteUri) 46 | { 47 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 48 | 49 | if (elementDescription == null 50 | || elementDescription.Type != ElementType.User) 51 | { 52 | return null; 53 | } 54 | 55 | return elementDescription.ID; 56 | } 57 | 58 | /// 59 | /// Fetch a Collection of User objects identified by the parameters provided. 60 | /// 61 | /// Optional. If included, will filter users by their name. 62 | /// Optional. If included, will filter users by their linked Twitch account username. 63 | /// Optional. If included, will filter users by their linked Hitbox account username. 64 | /// Optional. If included, will filter users by their linked Twitter account username.> 65 | /// Optional. If included, will filter users by their linked SpeedrunsLive account username. 66 | /// Optional. If included, will dictate the amount of elements included in each pagination. 67 | /// Optional. If omitted, users will be in the same order as the API. 68 | /// 69 | public IEnumerable GetUsers( 70 | string name = null, 71 | string twitch = null, string hitbox = null, 72 | string twitter = null, string speedrunslive = null, 73 | int? elementsPerPage = null, 74 | UsersOrdering orderBy = default) 75 | { 76 | var parameters = new List(); 77 | 78 | if (!string.IsNullOrEmpty(name)) 79 | { 80 | parameters.Add(string.Format("name={0}", 81 | Uri.EscapeDataString(name))); 82 | } 83 | 84 | if (!string.IsNullOrEmpty(twitch)) 85 | { 86 | parameters.Add(string.Format("twitch={0}", 87 | Uri.EscapeDataString(twitch))); 88 | } 89 | 90 | if (!string.IsNullOrEmpty(hitbox)) 91 | { 92 | parameters.Add(string.Format("hitbox={0}", 93 | Uri.EscapeDataString(hitbox))); 94 | } 95 | 96 | if (!string.IsNullOrEmpty(twitter)) 97 | { 98 | parameters.Add(string.Format("twitter={0}", 99 | Uri.EscapeDataString(twitter))); 100 | } 101 | 102 | if (!string.IsNullOrEmpty(speedrunslive)) 103 | { 104 | parameters.Add(string.Format("speedrunslive={0}", 105 | Uri.EscapeDataString(speedrunslive))); 106 | } 107 | 108 | if (elementsPerPage.HasValue) 109 | { 110 | parameters.Add(string.Format("max={0}", elementsPerPage)); 111 | } 112 | 113 | parameters.AddRange(orderBy.ToParameters()); 114 | 115 | Uri uri = GetUsersUri(parameters.ToParameters()); 116 | return baseClient.DoPaginatedRequest(uri, 117 | x => User.Parse(baseClient, x) as User); 118 | } 119 | 120 | /// 121 | /// Fetch a Collection of User objects identified by their fuzzy (vague) username. 122 | /// 123 | /// Optional. If included, dictates the fuzzy name of the user. 124 | /// Optional. If included, will dictate the amount of elements included in each pagination. 125 | /// Optional. If omitted, users will be in the same order as the API. 126 | /// 127 | public IEnumerable GetUsersFuzzy( 128 | string fuzzyName = null, 129 | int? elementsPerPage = null, 130 | UsersOrdering orderBy = default) 131 | { 132 | var parameters = new List(); 133 | 134 | if (!string.IsNullOrEmpty(fuzzyName)) 135 | { 136 | parameters.Add(string.Format("lookup={0}", 137 | Uri.EscapeDataString(fuzzyName))); 138 | } 139 | 140 | if (elementsPerPage.HasValue) 141 | { 142 | parameters.Add(string.Format("max={0}", elementsPerPage)); 143 | } 144 | 145 | parameters.AddRange(orderBy.ToParameters()); 146 | 147 | Uri uri = GetUsersUri(parameters.ToParameters()); 148 | return baseClient.DoPaginatedRequest(uri, 149 | x => User.Parse(baseClient, x) as User); 150 | } 151 | 152 | /// 153 | /// Fetch a User object identified by its ID. 154 | /// 155 | /// The ID of the user. 156 | /// 157 | public User GetUser(string userId) 158 | { 159 | Uri uri = GetUsersUri(string.Format("/{0}", 160 | Uri.EscapeDataString(userId))); 161 | 162 | dynamic result = baseClient.DoRequest(uri); 163 | 164 | return User.Parse(baseClient, result.data); 165 | } 166 | 167 | /// 168 | /// Fetch a Collection of Record objects of a user's personal bests identified by their ID. 169 | /// 170 | /// The ID of the user. 171 | /// Optional. If included, will dictate the amount of top runs included in the response. 172 | /// Optional. If included, will filter runs by their targetted game's series ID. 173 | /// Optional. If included, will filter runs by their targetted game's ID. 174 | /// Optional. If included, will dictate the additional resources embedded in the response. 175 | /// 176 | public ReadOnlyCollection GetPersonalBests( 177 | string userId, int? top = null, 178 | string seriesId = null, string gameId = null, 179 | RunEmbeds embeds = default) 180 | { 181 | var parameters = new List() { embeds.ToString() }; 182 | 183 | if (top.HasValue) 184 | { 185 | parameters.Add(string.Format("top={0}", top.Value)); 186 | } 187 | 188 | if (!string.IsNullOrEmpty(seriesId)) 189 | { 190 | parameters.Add(string.Format("series={0}", Uri.EscapeDataString(seriesId))); 191 | } 192 | 193 | if (!string.IsNullOrEmpty(gameId)) 194 | { 195 | parameters.Add(string.Format("game={0}", Uri.EscapeDataString(gameId))); 196 | } 197 | 198 | Uri uri = GetUsersUri(string.Format("/{0}/personal-bests{1}", 199 | Uri.EscapeDataString(userId), 200 | parameters.ToParameters())); 201 | 202 | return baseClient.DoDataCollectionRequest(uri, 203 | x => Record.Parse(baseClient, x) as Record); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Users/UsersOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Users in responses. 7 | /// 8 | public enum UsersOrdering : int 9 | { 10 | Name = 0, 11 | NameDescending, 12 | JapaneseName, 13 | JapaneseNameDescending, 14 | SignUpDate, 15 | SignUpDateDescending, 16 | Role, 17 | RoleDescending 18 | } 19 | 20 | internal static class UsersOrderingHelpers 21 | { 22 | internal static IEnumerable ToParameters(this UsersOrdering ordering) 23 | { 24 | bool isDescending = ((int)ordering & 1) == 1; 25 | if (isDescending) 26 | { 27 | ordering = (UsersOrdering)((int)ordering - 1); 28 | } 29 | 30 | string str = ""; 31 | 32 | switch (ordering) 33 | { 34 | case UsersOrdering.JapaneseName: 35 | str = "name.jap"; break; 36 | case UsersOrdering.SignUpDate: 37 | str = "signup"; break; 38 | case UsersOrdering.Role: 39 | str = "role"; break; 40 | } 41 | 42 | var list = new List(); 43 | 44 | if (!string.IsNullOrEmpty(str)) 45 | { 46 | list.Add(string.Format("orderby={0}", str)); 47 | } 48 | 49 | if (isDescending) 50 | { 51 | list.Add("direction=desc"); 52 | } 53 | 54 | return list; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/Variable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | 7 | namespace SpeedrunComSharp; 8 | 9 | public class Variable : IElementWithID 10 | { 11 | public string ID { get; private set; } 12 | public string Name { get; private set; } 13 | public VariableScope Scope { get; private set; } 14 | public bool IsMandatory { get; private set; } 15 | public bool IsUserDefined { get; private set; } 16 | public bool IsUsedForObsoletingRuns { get; private set; } 17 | public ReadOnlyCollection Values { get; private set; } 18 | public VariableValue DefaultValue { get; private set; } 19 | public bool IsSubcategory { get; private set; } 20 | 21 | #region Links 22 | 23 | private Lazy game; 24 | private Lazy category; 25 | private Lazy level; 26 | 27 | public string GameID { get; private set; } 28 | public Game Game => game.Value; 29 | public string CategoryID { get; private set; } 30 | public Category Category => category.Value; 31 | public Level Level => level.Value; 32 | 33 | #endregion 34 | 35 | private SpeedrunComClient client; 36 | 37 | private Variable() { } 38 | 39 | public VariableValue CreateCustomValue(string customValue) 40 | { 41 | if (!IsUserDefined) 42 | { 43 | throw new NotSupportedException("This variable doesn't support custom values."); 44 | } 45 | 46 | return VariableValue.CreateCustomValue(client, ID, customValue); 47 | } 48 | 49 | public static Variable Parse(SpeedrunComClient client, dynamic variableElement) 50 | { 51 | var variable = new Variable 52 | { 53 | client = client 54 | }; 55 | 56 | var properties = variableElement.Properties as IDictionary; 57 | var links = properties["links"] as IEnumerable; 58 | 59 | //Parse Attributes 60 | 61 | variable.ID = variableElement.id as string; 62 | variable.Name = variableElement.name as string; 63 | variable.Scope = VariableScope.Parse(client, variableElement.scope) as VariableScope; 64 | variable.IsMandatory = (bool)(variableElement.mandatory ?? false); 65 | variable.IsUserDefined = (bool)(properties["user-defined"] ?? false); 66 | variable.IsUsedForObsoletingRuns = (bool)variableElement.obsoletes; 67 | 68 | if (variableElement.values.choices is not ArrayList) 69 | { 70 | var choiceElements = variableElement.values.choices.Properties as IDictionary; 71 | variable.Values = choiceElements.Select(x => VariableValue.ParseIDPair(client, variable, x)).ToList().AsReadOnly(); 72 | } 73 | else 74 | { 75 | variable.Values = new ReadOnlyCollection(new VariableValue[0]); 76 | } 77 | 78 | var valuesProperties = variableElement.values.Properties as IDictionary; 79 | string defaultValue = valuesProperties["default"] as string; 80 | if (!string.IsNullOrEmpty(defaultValue)) 81 | { 82 | variable.DefaultValue = variable.Values.FirstOrDefault(x => x.ID == defaultValue); 83 | } 84 | 85 | variable.IsSubcategory = (bool)(properties["is-subcategory"] ?? false); 86 | 87 | //Parse Links 88 | 89 | dynamic gameLink = links.FirstOrDefault(x => x.rel == "game"); 90 | if (gameLink != null) 91 | { 92 | string gameUri = gameLink.uri as string; 93 | variable.GameID = gameUri[(gameUri.LastIndexOf("/") + 1)..]; 94 | variable.game = new Lazy(() => client.Games.GetGame(variable.GameID)); 95 | } 96 | else 97 | { 98 | variable.game = new Lazy(() => null); 99 | } 100 | 101 | variable.CategoryID = variableElement.category as string; 102 | if (!string.IsNullOrEmpty(variable.CategoryID)) 103 | { 104 | variable.category = new Lazy(() => client.Categories.GetCategory(variable.CategoryID)); 105 | } 106 | else 107 | { 108 | variable.category = new Lazy(() => null); 109 | } 110 | 111 | if (!string.IsNullOrEmpty(variable.Scope.LevelID)) 112 | { 113 | variable.level = new Lazy(() => client.Levels.GetLevel(variable.Scope.LevelID)); 114 | } 115 | else 116 | { 117 | variable.level = new Lazy(() => null); 118 | } 119 | 120 | return variable; 121 | } 122 | 123 | public override int GetHashCode() 124 | { 125 | return (ID ?? string.Empty).GetHashCode(); 126 | } 127 | 128 | public override bool Equals(object obj) 129 | { 130 | if (obj is not Variable other) 131 | { 132 | return false; 133 | } 134 | 135 | return ID == other.ID; 136 | } 137 | 138 | public override string ToString() 139 | { 140 | return Name; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/VariableScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class VariableScope 6 | { 7 | public VariableScopeType Type { get; private set; } 8 | public string LevelID { get; private set; } 9 | 10 | #region Links 11 | 12 | private Lazy level; 13 | 14 | public Level Level => level.Value; 15 | 16 | #endregion 17 | 18 | private VariableScope() { } 19 | 20 | private static VariableScopeType parseType(string type) 21 | { 22 | return type switch 23 | { 24 | "global" => VariableScopeType.Global, 25 | "full-game" => VariableScopeType.FullGame, 26 | "all-levels" => VariableScopeType.AllLevels, 27 | "single-level" => VariableScopeType.SingleLevel, 28 | _ => throw new ArgumentException("type"), 29 | }; 30 | } 31 | 32 | public static VariableScope Parse(SpeedrunComClient client, dynamic scopeElement) 33 | { 34 | var scope = new VariableScope 35 | { 36 | Type = parseType(scopeElement.type as string) 37 | }; 38 | 39 | if (scope.Type == VariableScopeType.SingleLevel) 40 | { 41 | scope.LevelID = scopeElement.level as string; 42 | scope.level = new Lazy(() => client.Levels.GetLevel(scope.LevelID)); 43 | } 44 | else 45 | { 46 | scope.level = new Lazy(() => null); 47 | } 48 | 49 | return scope; 50 | } 51 | 52 | public override string ToString() 53 | { 54 | if (Type == VariableScopeType.SingleLevel) 55 | { 56 | return "Single Level: " + (Level.Name ?? ""); 57 | } 58 | else 59 | { 60 | return Type.ToString(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/VariableScopeType.cs: -------------------------------------------------------------------------------- 1 | namespace SpeedrunComSharp; 2 | 3 | public enum VariableScopeType 4 | { 5 | Global, FullGame, AllLevels, SingleLevel 6 | } 7 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/VariableValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SpeedrunComSharp; 6 | 7 | public class VariableValue : IElementWithID 8 | { 9 | public string ID { get; private set; } 10 | 11 | public string VariableID { get; private set; } 12 | 13 | #region Links 14 | 15 | internal Lazy variable; 16 | internal Lazy value; 17 | 18 | public Variable Variable => variable.Value; 19 | public string Value => value.Value; 20 | public string Name => Variable.Name; 21 | 22 | public bool IsCustomValue => string.IsNullOrEmpty(ID); 23 | 24 | #endregion 25 | 26 | private VariableValue() { } 27 | 28 | public static VariableValue CreateCustomValue(SpeedrunComClient client, string variableId, string customValue) 29 | { 30 | var value = new VariableValue 31 | { 32 | VariableID = variableId 33 | }; 34 | 35 | value.variable = new Lazy(() => client.Variables.GetVariable(value.VariableID)); 36 | value.value = new Lazy(() => customValue); 37 | 38 | return value; 39 | } 40 | 41 | public static VariableValue ParseValueDescriptor(SpeedrunComClient client, KeyValuePair valueElement) 42 | { 43 | var value = new VariableValue 44 | { 45 | VariableID = valueElement.Key, 46 | ID = valueElement.Value as string 47 | }; 48 | 49 | //Parse Links 50 | 51 | value.variable = new Lazy(() => client.Variables.GetVariable(value.VariableID)); 52 | value.value = new Lazy(() => value.Variable.Values.FirstOrDefault(x => x.ID == value.ID).Value); 53 | 54 | return value; 55 | } 56 | 57 | public static VariableValue ParseIDPair(SpeedrunComClient client, Variable variable, KeyValuePair valueElement) 58 | { 59 | var value = new VariableValue 60 | { 61 | VariableID = variable.ID, 62 | ID = valueElement.Key, 63 | 64 | //Parse Links 65 | 66 | variable = new Lazy(() => variable) 67 | }; 68 | 69 | string valueName = valueElement.Value as string; 70 | value.value = new Lazy(() => valueName); 71 | 72 | return value; 73 | } 74 | 75 | public override int GetHashCode() 76 | { 77 | return (ID ?? string.Empty).GetHashCode(); 78 | } 79 | 80 | public override bool Equals(object obj) 81 | { 82 | if (obj is not VariableValue other) 83 | { 84 | return false; 85 | } 86 | 87 | return ID == other.ID; 88 | } 89 | 90 | public override string ToString() 91 | { 92 | return Value; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/VariablesClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | public class VariablesClient 6 | { 7 | public const string Name = "variables"; 8 | 9 | private readonly SpeedrunComClient baseClient; 10 | 11 | public VariablesClient(SpeedrunComClient baseClient) 12 | { 13 | this.baseClient = baseClient; 14 | } 15 | 16 | public static Uri GetVariablesUri(string subUri) 17 | { 18 | return SpeedrunComClient.GetAPIUri(string.Format("{0}{1}", Name, subUri)); 19 | } 20 | 21 | /// 22 | /// Fetch a Variable object identified by its URI. 23 | /// 24 | /// The site URI of the variable. 25 | /// 26 | public Variable GetVariableFromSiteUri(string siteUri) 27 | { 28 | string id = GetVariableIDFromSiteUri(siteUri); 29 | 30 | if (string.IsNullOrEmpty(id)) 31 | { 32 | return null; 33 | } 34 | 35 | return GetVariable(id); 36 | } 37 | 38 | /// 39 | /// Fetch a Variable ID identified by its URI. 40 | /// 41 | /// The site URI of the variable. 42 | /// 43 | public string GetVariableIDFromSiteUri(string siteUri) 44 | { 45 | ElementDescription elementDescription = baseClient.GetElementDescriptionFromSiteUri(siteUri); 46 | 47 | if (elementDescription == null 48 | || elementDescription.Type != ElementType.Variable) 49 | { 50 | return null; 51 | } 52 | 53 | return elementDescription.ID; 54 | } 55 | 56 | /// 57 | /// Fetch a Variable object identified by its ID. 58 | /// 59 | /// The ID of the variable. 60 | /// 61 | public Variable GetVariable(string variableId) 62 | { 63 | Uri uri = GetVariablesUri(string.Format("/{0}", 64 | Uri.EscapeDataString(variableId))); 65 | 66 | dynamic result = baseClient.DoRequest(uri); 67 | 68 | return Variable.Parse(baseClient, result.data); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/SpeedrunComSharp/Variables/VariablesOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SpeedrunComSharp; 4 | 5 | /// 6 | /// Options for ordering Variables in responses. 7 | /// 8 | public enum VariablesOrdering : int 9 | { 10 | Position = 0, 11 | PositionDescending, 12 | Name, 13 | NameDescending, 14 | Mandatory, 15 | MandatoryDescending, 16 | UserDefined, 17 | UserDefinedDescending 18 | } 19 | 20 | internal static class VariablesOrderHelpers 21 | { 22 | internal static IEnumerable ToParameters(this VariablesOrdering ordering) 23 | { 24 | bool isDescending = ((int)ordering & 1) == 1; 25 | if (isDescending) 26 | { 27 | ordering = (VariablesOrdering)((int)ordering - 1); 28 | } 29 | 30 | string str = ""; 31 | 32 | switch (ordering) 33 | { 34 | case VariablesOrdering.Name: 35 | str = "name"; break; 36 | case VariablesOrdering.Mandatory: 37 | str = "mandatory"; break; 38 | case VariablesOrdering.UserDefined: 39 | str = "user-defined"; break; 40 | } 41 | 42 | var list = new List(); 43 | 44 | if (!string.IsNullOrEmpty(str)) 45 | { 46 | list.Add(string.Format("orderby={0}", str)); 47 | } 48 | 49 | if (isDescending) 50 | { 51 | list.Add("direction=desc"); 52 | } 53 | 54 | return list; 55 | } 56 | } 57 | --------------------------------------------------------------------------------