Merge branch 'master' into changelog-overlay

This commit is contained in:
Dean Herbert
2019-05-13 00:24:18 +09:00
committed by GitHub
1624 changed files with 45678 additions and 21825 deletions

View File

@ -1,12 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading;
using osu.Framework.Configuration;
using Newtonsoft.Json.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
@ -20,7 +22,7 @@ namespace osu.Game.Online.API
private readonly OsuConfigManager config;
private readonly OAuth authentication;
public string Endpoint = @"https://osu.ppy.sh";
public string Endpoint => @"https://osu.ppy.sh";
private const string client_id = @"5";
private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
@ -62,24 +64,26 @@ namespace osu.Game.Online.API
thread.Start();
}
private void onTokenChanged(OAuthToken token) => config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
internal new void Schedule(Action action) => base.Schedule(action);
/// <summary>
/// Register a component to receive API events.
/// Fires <see cref="IOnlineComponent.APIStateChanged"/> once immediately to ensure a correct state.
/// </summary>
/// <param name="component"></param>
public void Register(IOnlineComponent component)
{
Scheduler.Add(delegate
{
components.Add(component);
component.APIStateChanged(this, state);
});
Schedule(() => components.Add(component));
component.APIStateChanged(this, state);
}
public void Unregister(IOnlineComponent component)
{
Scheduler.Add(delegate { components.Remove(component); });
Schedule(() => components.Remove(component));
}
public string AccessToken => authentication.RequestAccessToken();
@ -99,13 +103,17 @@ namespace osu.Game.Online.API
//todo: replace this with a ping request.
log.Add(@"In a failing state, waiting a bit before we try again...");
Thread.Sleep(5000);
if (!IsLoggedIn) goto case APIState.Connecting;
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new ListChannelsRequest { Timeout = 5000 });
Queue(new GetUserRequest());
}
break;
case APIState.Offline:
case APIState.Connecting:
//work to restore a connection...
@ -143,7 +151,8 @@ namespace osu.Game.Online.API
if (!handleRequest(userReq))
{
Thread.Sleep(500);
if (State == APIState.Connecting)
State = APIState.Failing;
continue;
}
@ -159,7 +168,7 @@ namespace osu.Game.Online.API
//hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout(false);
Logout();
continue;
}
@ -170,10 +179,10 @@ namespace osu.Game.Online.API
lock (queue)
{
if (queue.Count == 0) break;
req = queue.Dequeue();
}
// TODO: handle failures better
handleRequest(req);
}
@ -189,66 +198,65 @@ namespace osu.Game.Online.API
this.password = password;
}
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Debug.Assert(State == APIState.Offline);
var req = new RegistrationRequest
{
Url = $@"{Endpoint}/users",
Method = HttpMethod.Post,
Username = username,
Email = email,
Password = password
};
try
{
req.Perform();
}
catch (Exception e)
{
try
{
return JObject.Parse(req.ResponseString).SelectToken("form_error", true).ToObject<RegistrationRequest.RegistrationRequestErrors>();
}
catch
{
// if we couldn't deserialize the error message let's throw the original exception outwards.
throw e;
}
}
return null;
}
/// <summary>
/// Handle a single API request.
/// Ensures all exceptions are caught and dealt with correctly.
/// </summary>
/// <param name="req">The request.</param>
/// <returns>true if we should remove this request from the queue.</returns>
/// <returns>true if the request succeeded.</returns>
private bool handleRequest(APIRequest req)
{
try
{
Logger.Log($@"Performing request {req}", LoggingTarget.Network);
req.Perform(this);
//we could still be in initialisation, at which point we don't want to say we're Online yet.
if (IsLoggedIn)
State = APIState.Online;
if (IsLoggedIn) State = APIState.Online;
failureCount = 0;
return true;
}
catch (WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode
?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
// special cases for un-typed but useful message responses.
switch (we.Message)
{
case "Unauthorized":
statusCode = HttpStatusCode.Unauthorized;
break;
}
switch (statusCode)
{
case HttpStatusCode.Unauthorized:
Logout(false);
return true;
case HttpStatusCode.RequestTimeout:
failureCount++;
log.Add($@"API failure count is now {failureCount}");
if (failureCount < 3)
//we might try again at an api level.
return false;
State = APIState.Failing;
flushQueue();
return true;
}
req.Fail(we);
return true;
handleWebException(we);
return false;
}
catch (Exception e)
catch
{
if (e is TimeoutException)
log.Add(@"API level timeout exception was hit");
req.Fail(e);
return true;
return false;
}
}
@ -256,26 +264,64 @@ namespace osu.Game.Online.API
public APIState State
{
get { return state; }
get => state;
private set
{
APIState oldState = state;
APIState newState = value;
if (state == value)
return;
APIState oldState = state;
state = value;
if (oldState != newState)
log.Add($@"We just went {state}!");
Schedule(() =>
{
log.Add($@"We just went {newState}!");
Scheduler.Add(delegate
{
components.ForEach(c => c.APIStateChanged(this, newState));
OnStateChange?.Invoke(oldState, newState);
});
}
components.ForEach(c => c.APIStateChanged(this, state));
OnStateChange?.Invoke(oldState, state);
});
}
}
private bool handleWebException(WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode
?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
// special cases for un-typed but useful message responses.
switch (we.Message)
{
case "Unauthorized":
case "Forbidden":
statusCode = HttpStatusCode.Unauthorized;
break;
}
switch (statusCode)
{
case HttpStatusCode.Unauthorized:
Logout();
return true;
case HttpStatusCode.RequestTimeout:
failureCount++;
log.Add($@"API failure count is now {failureCount}");
if (failureCount < 3)
//we might try again at an api level.
return false;
if (State == APIState.Online)
{
State = APIState.Failing;
flushQueue();
}
return true;
}
return true;
}
public bool IsLoggedIn => LocalUser.Value.Id > 1;
public void Queue(APIRequest request)
@ -303,21 +349,20 @@ namespace osu.Game.Online.API
}
}
public void Logout(bool clearUsername = true)
public void Logout()
{
flushQueue();
if (clearUsername) ProvidedUsername = null;
password = null;
authentication.Clear();
LocalUser.Value = createGuestUser();
// Scheduled prior to state change such that the state changed event is invoked with the correct user present
Schedule(() => LocalUser.Value = createGuestUser());
State = APIState.Offline;
}
private static User createGuestUser() => new User
{
Username = @"Guest",
Id = 1,
};
private static User createGuestUser() => new GuestUser();
protected override void Dispose(bool isDisposing)
{
@ -328,6 +373,15 @@ namespace osu.Game.Online.API
}
}
internal class GuestUser : User
{
public GuestUser()
{
Username = @"Guest";
Id = 1;
}
}
public enum APIState
{
/// <summary>

View File

@ -1,20 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public abstract class APIDownloadRequest : APIRequest
{
private string filename;
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
var request = new FileWebRequest(filename = Path.GetTempFileName(), Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Schedule(() => Progress?.Invoke(current, total));
private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total));
protected APIDownloadRequest()
{
@ -23,11 +26,11 @@ namespace osu.Game.Online.API
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
Success?.Invoke(filename);
}
public event APIProgressHandler Progress;
public event APIProgressHandler Progressed;
public new event APISuccessHandler<byte[]> Success;
public new event APISuccessHandler<string> Success;
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API
{
public abstract class APIMessagesRequest : APIRequest<List<Message>>
{
private readonly long? sinceId;
protected APIMessagesRequest(long? sinceId)
{
this.sinceId = sinceId;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (sinceId.HasValue) req.AddParameter(@"since", sinceId.Value.ToString());
return req;
}
}
}

View File

@ -1,8 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
namespace osu.Game.Online.API
{
@ -35,23 +36,12 @@ namespace osu.Game.Online.API
/// </summary>
public abstract class APIRequest
{
/// <summary>
/// The maximum amount of time before this request will fail.
/// </summary>
public int Timeout = WebRequest.DEFAULT_TIMEOUT;
protected virtual string Target => string.Empty;
protected abstract string Target { get; }
protected virtual WebRequest CreateWebRequest() => new WebRequest(Uri);
protected virtual string Uri => $@"{API.Endpoint}/api/v2/{Target}";
private double remainingTime => Math.Max(0, Timeout - (DateTimeOffset.UtcNow - (startTime ?? DateTimeOffset.MinValue)).TotalMilliseconds);
public bool ExceededTimeout => remainingTime == 0;
private DateTimeOffset? startTime;
protected APIAccess API;
protected WebRequest WebRequest;
@ -71,53 +61,59 @@ namespace osu.Game.Online.API
private Action pendingFailure;
public void Perform(APIAccess api)
public void Perform(IAPIProvider api)
{
API = api;
if (!(api is APIAccess apiAccess))
throw new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.");
if (checkAndProcessFailure())
API = apiAccess;
if (checkAndScheduleFailure())
return;
if (startTime == null)
startTime = DateTimeOffset.UtcNow;
if (remainingTime <= 0)
throw new TimeoutException(@"API request timeout hit");
WebRequest = CreateWebRequest();
WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}");
WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
if (checkAndProcessFailure())
if (checkAndScheduleFailure())
return;
if (!WebRequest.Aborted) //could have been aborted by a Cancel() call
{
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
}
if (checkAndProcessFailure())
if (checkAndScheduleFailure())
return;
api.Schedule(delegate { Success?.Invoke(); });
API.Schedule(delegate { Success?.Invoke(); });
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
{
cancelled = true;
if (WebRequest?.Completed == true)
return;
if (cancelled)
return;
cancelled = true;
WebRequest?.Abort();
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
pendingFailure = () => Failure?.Invoke(e);
checkAndProcessFailure();
checkAndScheduleFailure();
}
/// <summary>
/// Checked for cancellation or error. Also queues up the Failed event if we can.
/// </summary>
/// <returns>Whether we are in a failed or cancelled state.</returns>
private bool checkAndProcessFailure()
private bool checkAndScheduleFailure()
{
if (API == null || pendingFailure == null) return cancelled;
@ -128,7 +124,10 @@ namespace osu.Game.Online.API
}
public delegate void APIFailureHandler(Exception e);
public delegate void APISuccessHandler();
public delegate void APIProgressHandler(long current, long total);
public delegate void APISuccessHandler<in T>(T content);
}

View File

@ -1,23 +1,44 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Configuration;
using System.Collections.Generic;
using System.Threading;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public class DummyAPIAccess : IAPIProvider
public class DummyAPIAccess : Component, IAPIProvider
{
public Bindable<User> LocalUser { get; } = new Bindable<User>(new User
{
Username = @"Dummy",
Id = 1,
Id = 1001,
});
public bool IsLoggedIn => true;
public void Update()
public string ProvidedUsername => LocalUser.Value.Username;
public string Endpoint => "http://localhost";
private APIState state = APIState.Online;
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
public APIState State
{
get => state;
private set
{
if (state == value)
return;
state = value;
Scheduler.Add(() => components.ForEach(c => c.APIStateChanged(this, value)));
}
}
public virtual void Queue(APIRequest request)
@ -26,6 +47,36 @@ namespace osu.Game.Online.API
public void Register(IOnlineComponent component)
{
Scheduler.Add(delegate { components.Add(component); });
component.APIStateChanged(this, state);
}
public void Unregister(IOnlineComponent component)
{
Scheduler.Add(delegate { components.Remove(component); });
}
public void Login(string username, string password)
{
LocalUser.Value = new User
{
Username = username,
Id = 1001,
};
State = APIState.Online;
}
public void Logout()
{
LocalUser.Value = new GuestUser();
State = APIState.Offline;
}
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Thread.Sleep(200);
return null;
}
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Configuration;
using osu.Framework.Bindables;
using osu.Game.Users;
namespace osu.Game.Online.API
@ -18,6 +18,19 @@ namespace osu.Game.Online.API
/// </summary>
bool IsLoggedIn { get; }
/// <summary>
/// The last username provided by the end-user.
/// May not be authenticated.
/// </summary>
string ProvidedUsername { get; }
/// <summary>
/// The URL endpoint for this API. Does not include a trailing slash.
/// </summary>
string Endpoint { get; }
APIState State { get; }
/// <summary>
/// Queue a new request.
/// </summary>
@ -29,5 +42,32 @@ namespace osu.Game.Online.API
/// </summary>
/// <param name="component">The component to register.</param>
void Register(IOnlineComponent component);
/// <summary>
/// Unregisters a component to receive state changes.
/// </summary>
/// <param name="component">The component to unregister.</param>
void Unregister(IOnlineComponent component);
/// <summary>
/// Attempt to login using the provided credentials. This is a non-blocking operation.
/// </summary>
/// <param name="username">The user's username.</param>
/// <param name="password">The user's password.</param>
void Login(string username, string password);
/// <summary>
/// Log out the current user.
/// </summary>
void Logout();
/// <summary>
/// Create a new user account. This is a blocking operation.
/// </summary>
/// <param name="email">The email to create the account with.</param>
/// <param name="username">The username to create the account with.</param>
/// <param name="password">The password to create the account with.</param>
/// <returns>Any errors encoutnered during account creation.</returns>
RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password);
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.API
{
public interface IOnlineComponent
{
void APIStateChanged(APIAccess api, APIState state);
void APIStateChanged(IAPIProvider api, APIState state);
}
}

View File

@ -1,9 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Net.Http;
using osu.Framework.Configuration;
using osu.Framework.Bindables;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
@ -178,6 +178,7 @@ namespace osu.Game.Online.API
AddParameter("grant_type", GrantType);
AddParameter("client_id", ClientId);
AddParameter("client_secret", ClientSecret);
AddParameter("scope", "*");
base.PrePerform();
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
@ -19,15 +19,8 @@ namespace osu.Game.Online.API
[JsonProperty(@"expires_in")]
public long ExpiresIn
{
get
{
return AccessTokenExpiry - DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
set
{
AccessTokenExpiry = DateTimeOffset.Now.AddSeconds(value).ToUnixTimeSeconds();
}
get => AccessTokenExpiry - DateTimeOffset.UtcNow.ToUnixTimeSeconds();
set => AccessTokenExpiry = DateTimeOffset.Now.AddSeconds(value).ToUnixTimeSeconds();
}
public bool IsValid => !string.IsNullOrEmpty(AccessToken) && ExpiresIn > 30;
@ -57,6 +50,7 @@ namespace osu.Game.Online.API
catch
{
}
return null;
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public class RegistrationRequest : WebRequest
{
internal string Username;
internal string Email;
internal string Password;
protected override void PrePerform()
{
AddParameter("user[username]", Username);
AddParameter("user[user_email]", Email);
AddParameter("user[password]", Password);
base.PrePerform();
}
public class RegistrationRequestErrors
{
public UserErrors User;
public class UserErrors
{
[JsonProperty("username")]
public string[] Username;
[JsonProperty("user_email")]
public string[] Email;
[JsonProperty("password")]
public string[] Password;
}
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class CreateNewPrivateMessageRequest : APIRequest<CreateNewPrivateMessageResponse>
{
private readonly User user;
private readonly Message message;
public CreateNewPrivateMessageRequest(User user, Message message)
{
this.user = user;
this.message = message;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter(@"target_id", user.Id.ToString());
req.AddParameter(@"message", message.Content);
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
return req;
}
protected override string Target => @"chat/new";
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class CreateNewPrivateMessageResponse
{
[JsonProperty("new_channel_id")]
public int ChannelID;
public Message Message;
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.API.Requests
{
public class CreateRoomRequest : APIRequest<APICreatedRoom>
{
private readonly Room room;
public CreateRoomRequest(Room room)
{
this.room = room;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.ContentType = "application/json";
req.Method = HttpMethod.Post;
req.AddRaw(JsonConvert.SerializeObject(room));
return req;
}
protected override string Target => "rooms";
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class CreateRoomScoreRequest : APIRequest<APIScoreToken>
{
private readonly int roomId;
private readonly int playlistItemId;
public CreateRoomScoreRequest(int roomId, int playlistItemId)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
return req;
}
protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores";
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using System;
@ -10,7 +10,9 @@ namespace osu.Game.Online.API.Requests
{
public readonly BeatmapSetInfo BeatmapSet;
public Action<float> DownloadProgressed;
public float Progress;
public event Action<float> DownloadProgressed;
private readonly bool noVideo;
@ -19,7 +21,7 @@ namespace osu.Game.Online.API.Requests
this.noVideo = noVideo;
BeatmapSet = set;
Progress += (current, total) => DownloadProgressed?.Invoke((float) current / total);
Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total);
}
protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Users;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.Chat;

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetRoomScoresRequest : APIRequest<List<APIRoomScoreInfo>>
{
private readonly int roomId;
public GetRoomScoresRequest(int roomId)
{
this.roomId = roomId;
}
protected override string Target => $@"rooms/{roomId}/leaderboard";
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Online.API.Requests
{
public class GetRoomsRequest : APIRequest<List<Room>>
{
private readonly PrimaryFilter primaryFilter;
public GetRoomsRequest(PrimaryFilter primaryFilter)
{
this.primaryFilter = primaryFilter;
}
protected override string Target
{
get
{
string target = "rooms";
switch (primaryFilter)
{
case PrimaryFilter.Open:
break;
case PrimaryFilter.Owned:
target += "/owned";
break;
case PrimaryFilter.Participated:
target += "/participated";
break;
case PrimaryFilter.RecentlyEnded:
target += "/ended";
break;
}
return target;
}
}
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
@ -10,18 +10,18 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<APIScores>
public class GetScoresRequest : APIRequest<APILegacyScores>
{
private readonly BeatmapInfo beatmap;
private readonly LeaderboardScope scope;
private readonly BeatmapLeaderboardScope scope;
private readonly RulesetInfo ruleset;
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, LeaderboardScope scope = LeaderboardScope.Global)
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global)
{
if (!beatmap.OnlineBeatmapID.HasValue)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}.");
if (scope == LeaderboardScope.Local)
if (scope == BeatmapLeaderboardScope.Local)
throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard");
this.beatmap = beatmap;
@ -31,10 +31,10 @@ namespace osu.Game.Online.API.Requests
Success += onSuccess;
}
private void onSuccess(APIScores r)
private void onSuccess(APILegacyScores r)
{
foreach (APIScore score in r.Scores)
score.ApplyBeatmap(beatmap);
foreach (APILegacyScoreInfo score in r.Scores)
score.Beatmap = beatmap;
}
protected override WebRequest CreateWebRequest()

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.IO.Network;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using System.Collections.Generic;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Users;

View File

@ -1,12 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetUserScoresRequest : APIRequest<List<APIScore>>
public class GetUserScoresRequest : APIRequest<List<APILegacyScoreInfo>>
{
private readonly long userId;
private readonly ScoreType type;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class JoinRoomRequest : APIRequest
{
private readonly Room room;
private readonly User user;
public JoinRoomRequest(Room room, User user)
{
this.room = room;
this.user = user;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Put;
return req;
}
protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}";
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Online.Chat;

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class PartRoomRequest : APIRequest
{
private readonly Room room;
private readonly User user;
public PartRoomRequest(Room room, User user)
{
this.room = room;
this.user = user;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Delete;
return req;
}
protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}";
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Beatmaps;
@ -59,19 +59,17 @@ namespace osu.Game.Online.API.Requests.Responses
public BeatmapInfo ToBeatmap(RulesetStore rulesets)
{
var set = BeatmapSet?.ToBeatmapSet(rulesets);
return new BeatmapInfo
{
Metadata = this,
Metadata = set?.Metadata ?? this,
Ruleset = rulesets.GetRuleset(ruleset),
StarDifficulty = starDifficulty,
OnlineBeatmapID = OnlineBeatmapID,
Version = version,
Status = Status,
BeatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = OnlineBeatmapSetID,
Status = BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None
},
BeatmapSet = set,
BaseDifficulty = new BeatmapDifficulty
{
DrainRate = drainRate,

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Beatmaps;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.API.Requests.Responses
{
public class APICreatedRoom : Room
{
[JsonProperty("error")]
public string Error { get; set; }
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
@ -7,16 +7,15 @@ using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScore : Score
public class APILegacyScoreInfo : LegacyScoreInfo
{
[JsonProperty(@"score")]
private double totalScore
private int totalScore
{
set => TotalScore = value;
}
@ -33,15 +32,6 @@ namespace osu.Game.Online.API.Requests.Responses
set => User = value;
}
[JsonProperty(@"replay_data")]
private Replay replay
{
set => Replay = value;
}
[JsonProperty(@"mode_int")]
public int OnlineRulesetID { get; set; }
[JsonProperty(@"score_id")]
private long onlineScoreID
{
@ -74,51 +64,79 @@ namespace osu.Game.Online.API.Requests.Responses
}
[JsonProperty(@"statistics")]
private Dictionary<string, object> jsonStats
private Dictionary<string, int> jsonStats
{
set
{
foreach (var kvp in value)
{
HitResult newKey;
switch (kvp.Key)
{
case @"count_geki":
CountGeki = kvp.Value;
break;
case @"count_300":
newKey = HitResult.Great;
Count300 = kvp.Value;
break;
case @"count_katu":
CountKatu = kvp.Value;
break;
case @"count_100":
newKey = HitResult.Good;
Count100 = kvp.Value;
break;
case @"count_50":
newKey = HitResult.Meh;
Count50 = kvp.Value;
break;
case @"count_miss":
newKey = HitResult.Miss;
CountMiss = kvp.Value;
break;
default:
continue;
}
Statistics.Add(newKey, kvp.Value);
}
}
}
[JsonProperty(@"mode_int")]
public int OnlineRulesetID
{
get => RulesetID;
set => RulesetID = value;
}
[JsonProperty(@"mods")]
private string[] modStrings { get; set; }
public void ApplyBeatmap(BeatmapInfo beatmap)
public override BeatmapInfo Beatmap
{
Beatmap = beatmap;
ApplyRuleset(beatmap.Ruleset);
get => base.Beatmap;
set
{
base.Beatmap = value;
if (Beatmap.Ruleset != null)
Ruleset = value.Ruleset;
}
}
public void ApplyRuleset(RulesetInfo ruleset)
public override RulesetInfo Ruleset
{
Ruleset = ruleset;
get => base.Ruleset;
set
{
base.Ruleset = value;
// Evaluate the mod string
Mods = Ruleset.CreateInstance().GetAllMods().Where(mod => modStrings.Contains(mod.ShortenedName)).ToArray();
if (modStrings != null)
{
// Evaluate the mod string
Mods = Ruleset.CreateInstance().GetAllMods().Where(mod => modStrings.Contains(mod.Acronym)).ToArray();
}
}
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APILegacyScores
{
[JsonProperty(@"scores")]
public List<APILegacyScoreInfo> Scores;
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIMod : IMod
{
public string Acronym { get; set; }
public bool Equals(IMod other) => Acronym == other?.Acronym;
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Humanizer;
using Newtonsoft.Json;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIRoomScoreInfo : ScoreInfo
{
[JsonProperty("attempts")]
public int TotalAttempts { get; set; }
[JsonProperty("completed")]
public int CompletedBeatmaps { get; set; }
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScoreToken
{
[JsonProperty("id")]
public int ID { get; set; }
}
}

View File

@ -1,14 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScores
{
[JsonProperty(@"scores")]
public IEnumerable<APIScore> Scores;
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Users;

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Beatmaps;
@ -10,16 +10,16 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIUserMostPlayedBeatmap
{
[JsonProperty("beatmap_id")]
public int BeatmapID;
public int BeatmapID { get; set; }
[JsonProperty("count")]
public int PlayCount;
public int PlayCount { get; set; }
[JsonProperty]
private BeatmapInfo beatmap;
private BeatmapInfo beatmap { get; set; }
[JsonProperty]
private APIBeatmapSet beatmapSet;
private APIBeatmapSet beatmapSet { get; set; }
public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets)
{

View File

@ -1,16 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.ComponentModel;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest<IEnumerable<APIBeatmapSet>>
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
{
private readonly string query;
private readonly RulesetInfo ruleset;
@ -35,14 +33,17 @@ namespace osu.Game.Online.API.Requests
public enum BeatmapSearchCategory
{
Any = 7,
[Description("Ranked & Approved")]
RankedApproved = 0,
Approved = 1,
Qualified = 3,
Loved = 8,
Favourites = 2,
Qualified = 3,
Pending = 4,
[Description("Pending & WIP")]
PendingWIP = 4,
Graveyard = 5,
[Description("My Maps")]
MyMaps = 6,
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsResponse
{
public IEnumerable<APIBeatmapSet> BeatmapSets;
/// <summary>
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
/// </summary>
[JsonProperty("cursor")]
public dynamic CursorJson;
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests
{
public class SubmitRoomScoreRequest : APIRequest
{
private readonly int scoreId;
private readonly int roomId;
private readonly int playlistItemId;
private readonly ScoreInfo scoreInfo;
public SubmitRoomScoreRequest(int scoreId, int roomId, int playlistItemId, ScoreInfo scoreInfo)
{
this.scoreId = scoreId;
this.roomId = roomId;
this.playlistItemId = playlistItemId;
this.scoreInfo = scoreInfo;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
req.AddRaw(JsonConvert.SerializeObject(scoreInfo));
return req;
}
protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}";
}
}

View File

@ -1,17 +1,65 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Configuration;
using osu.Framework.Bindables;
using osu.Framework.Lists;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class Channel
{
public readonly int MaxHistory = 300;
/// <summary>
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
/// </summary>
public readonly ObservableCollection<User> Users = new ObservableCollection<User>();
[JsonProperty(@"users")]
private long[] userIds
{
set
{
foreach (var id in value)
Users.Add(new User { Id = id });
}
}
/// <summary>
/// Contains all the messages send in the channel.
/// </summary>
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
/// <summary>
/// Contains all the messages that are still pending for submission to the server.
/// </summary>
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
/// <summary>
/// An event that fires when new messages arrived.
/// </summary>
public event Action<IEnumerable<Message>> NewMessagesArrived;
/// <summary>
/// An event that fires when a pending message gets resolved.
/// </summary>
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
/// <summary>
/// An event that fires when a pending message gets removed.
/// </summary>
public event Action<Message> MessageRemoved;
public bool ReadOnly => false; //todo not yet used.
public override string ToString() => Name;
[JsonProperty(@"name")]
public string Name;
@ -22,19 +70,16 @@ namespace osu.Game.Online.Chat
public ChannelType Type;
[JsonProperty(@"channel_id")]
public int Id;
public long Id;
[JsonProperty(@"last_message_id")]
public long? LastMessageId;
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
/// <summary>
/// Signalles if the current user joined this channel or not. Defaults to false.
/// </summary>
public Bindable<bool> Joined = new Bindable<bool>();
public bool ReadOnly => false;
public const int MAX_HISTORY = 300;
[JsonConstructor]
@ -42,10 +87,21 @@ namespace osu.Game.Online.Chat
{
}
public event Action<IEnumerable<Message>> NewMessagesArrived;
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
public event Action<Message> MessageRemoved;
/// <summary>
/// Create a private messaging channel with the specified user.
/// </summary>
/// <param name="user">The user to create the private conversation with.</param>
public Channel(User user)
{
Type = ChannelType.PM;
Users.Add(user);
Name = user.Username;
}
/// <summary>
/// Adds the argument message as a local echo. When this local echo is resolved <see cref="PendingMessageResolved"/> will get called.
/// </summary>
/// <param name="message"></param>
public void AddLocalEcho(LocalEchoMessage message)
{
pendingMessages.Add(message);
@ -54,8 +110,12 @@ namespace osu.Game.Online.Chat
NewMessagesArrived?.Invoke(new[] { message });
}
public bool MessagesLoaded { get; private set; }
public bool MessagesLoaded;
/// <summary>
/// Adds new messages to the channel and purges old messages. Triggers the <see cref="NewMessagesArrived"/> event.
/// </summary>
/// <param name="messages"></param>
public void AddNewMessages(params Message[] messages)
{
messages = messages.Except(Messages).ToArray();
@ -63,7 +123,6 @@ namespace osu.Game.Online.Chat
if (messages.Length == 0) return;
Messages.AddRange(messages);
MessagesLoaded = true;
var maxMessageId = messages.Max(m => m.Id);
if (maxMessageId > LastMessageId)
@ -74,14 +133,6 @@ namespace osu.Game.Online.Chat
NewMessagesArrived?.Invoke(messages);
}
private void purgeOldMessages()
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MAX_HISTORY)
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
}
/// <summary>
/// Replace or remove a message from the channel.
/// </summary>
@ -101,17 +152,18 @@ namespace osu.Game.Online.Chat
}
if (Messages.Contains(final))
{
// message already inserted, so let's throw away this update.
// we may want to handle this better in the future, but for the time being api requests are single-threaded so order is assumed.
MessageRemoved?.Invoke(echo);
return;
}
throw new InvalidOperationException("Attempted to add the same message again");
Messages.Add(final);
PendingMessageResolved?.Invoke(echo, final);
}
public override string ToString() => Name;
private void purgeOldMessages()
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MaxHistory)
Messages.RemoveRange(0, messageCount - MaxHistory);
}
}
}

View File

@ -0,0 +1,443 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Chat.Tabs;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
/// <summary>
/// Manages everything channel related
/// </summary>
public class ChannelManager : PollingComponent
{
/// <summary>
/// The channels the player joins on startup
/// </summary>
private readonly string[] defaultChannels =
{
@"#lazer",
@"#osu",
@"#lobby"
};
private readonly BindableList<Channel> availableChannels = new BindableList<Channel>();
private readonly BindableList<Channel> joinedChannels = new BindableList<Channel>();
/// <summary>
/// The currently opened channel
/// </summary>
public Bindable<Channel> CurrentChannel { get; } = new Bindable<Channel>();
/// <summary>
/// The Channels the player has joined
/// </summary>
public IBindableList<Channel> JoinedChannels => joinedChannels;
/// <summary>
/// The channels available for the player to join
/// </summary>
public IBindableList<Channel> AvailableChannels => availableChannels;
private IAPIProvider api;
public readonly BindableBool HighPollRate = new BindableBool();
public ChannelManager()
{
CurrentChannel.ValueChanged += currentChannelChanged;
HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true);
}
/// <summary>
/// Opens a channel or switches to the channel if already opened.
/// </summary>
/// <exception cref="ChannelNotFoundException">If the name of the specifed channel was not found this exception will be thrown.</exception>
/// <param name="name"></param>
public void OpenChannel(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
}
/// <summary>
/// Opens a new private channel.
/// </summary>
/// <param name="user">The user the private channel is opened with.</param>
public void OpenPrivateChannel(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id))
?? new Channel(user);
}
private void currentChannelChanged(ValueChangedEvent<Channel> e)
{
if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel))
JoinChannel(e.NewValue);
}
/// <summary>
/// Ensure we run post actions in sequence, once at a time.
/// </summary>
private readonly Queue<Action> postQueue = new Queue<Action>();
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
/// <param name="target">An optional target channel. If null, <see cref="CurrentChannel"/> will be used.</param>
public void PostMessage(string text, bool isAction = false, Channel target = null)
{
if (target == null)
target = CurrentChannel.Value;
if (target == null)
return;
void dequeueAndRun()
{
if (postQueue.Count > 0)
postQueue.Dequeue().Invoke();
}
postQueue.Enqueue(() =>
{
if (!api.IsLoggedIn)
{
target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!"));
return;
}
var message = new LocalEchoMessage
{
Sender = api.LocalUser.Value,
Timestamp = DateTimeOffset.Now,
ChannelId = target.Id,
IsAction = isAction,
Content = text
};
target.AddLocalEcho(message);
// if this is a PM and the first message, we need to do a special request to create the PM channel
if (target.Type == ChannelType.PM && !target.Joined.Value)
{
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message);
createNewPrivateMessageRequest.Success += createRes =>
{
target.Id = createRes.ChannelID;
target.ReplaceMessage(message, createRes.Message);
dequeueAndRun();
};
createNewPrivateMessageRequest.Failure += exception =>
{
Logger.Error(exception, "Posting message failed.");
target.ReplaceMessage(message, null);
dequeueAndRun();
};
api.Queue(createNewPrivateMessageRequest);
return;
}
var req = new PostMessageRequest(message);
req.Success += m =>
{
target.ReplaceMessage(message, m);
dequeueAndRun();
};
req.Failure += exception =>
{
Logger.Error(exception, "Posting message failed.");
target.ReplaceMessage(message, null);
dequeueAndRun();
};
api.Queue(req);
});
// always run if the queue is empty
if (postQueue.Count == 1)
dequeueAndRun();
}
/// <summary>
/// Posts a command locally. Commands like /help will result in a help message written in the current channel.
/// </summary>
/// <param name="text">the text containing the command identifier and command parameters.</param>
/// <param name="target">An optional target channel. If null, <see cref="CurrentChannel"/> will be used.</param>
public void PostCommand(string text, Channel target = null)
{
if (target == null)
target = CurrentChannel.Value;
if (target == null)
return;
var parameters = text.Split(new[] { ' ' }, 2);
string command = parameters[0];
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
switch (command)
{
case "me":
if (string.IsNullOrWhiteSpace(content))
{
target.AddNewMessages(new ErrorMessage("Usage: /me [action]"));
break;
}
PostMessage(content, true);
break;
case "help":
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]"));
break;
default:
target.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help"));
break;
}
}
private void handleChannelMessages(IEnumerable<Message> messages)
{
var channels = JoinedChannels.ToList();
foreach (var group in messages.GroupBy(m => m.ChannelId))
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
}
private void initializeChannels()
{
var req = new ListChannelsRequest();
var joinDefaults = JoinedChannels.Count == 0;
req.Success += channels =>
{
foreach (var channel in channels)
{
var ch = getChannel(channel, addToAvailable: true);
// join any channels classified as "defaults"
if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase)))
JoinChannel(ch);
}
};
req.Failure += error =>
{
Logger.Error(error, "Fetching channel list failed");
initializeChannels();
};
api.Queue(req);
}
/// <summary>
/// Fetches inital messages of a channel
///
/// TODO: remove this when the API supports returning initial fetch messages for more than one channel by specifying the last message id per channel instead of one last message id globally.
/// right now it caps out at 50 messages and therefore only returns one channel's worth of content.
/// </summary>
/// <param name="channel">The channel </param>
private void fetchInitalMessages(Channel channel)
{
if (channel.Id <= 0) return;
var fetchInitialMsgReq = new GetMessagesRequest(channel);
fetchInitialMsgReq.Success += messages =>
{
handleChannelMessages(messages);
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
};
api.Queue(fetchInitialMsgReq);
}
/// <summary>
/// Find an existing channel instance for the provided channel. Lookup is performed basd on ID.
/// The provided channel may be used if an existing instance is not found.
/// </summary>
/// <param name="lookup">A candidate channel to be used for lookup or permanently on lookup failure.</param>
/// <param name="addToAvailable">Whether the channel should be added to <see cref="AvailableChannels"/> if not already.</param>
/// <param name="addToJoined">Whether the channel should be added to <see cref="JoinedChannels"/> if not already.</param>
/// <returns>The found channel.</returns>
private Channel getChannel(Channel lookup, bool addToAvailable = false, bool addToJoined = false)
{
Channel found = null;
bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : lookup.Name == ch.Name;
var available = AvailableChannels.FirstOrDefault(lookupCondition);
if (available != null)
found = available;
var joined = JoinedChannels.FirstOrDefault(lookupCondition);
if (found == null && joined != null)
found = joined;
if (found == null)
{
found = lookup;
// if we're using a channel object from the server, we want to remove ourselves from the users list.
// this is because we check the first user in the channel to display a name/icon on tabs for now.
var foundSelf = found.Users.FirstOrDefault(u => u.Id == api.LocalUser.Value.Id);
if (foundSelf != null)
found.Users.Remove(foundSelf);
}
if (joined == null && addToJoined) joinedChannels.Add(found);
if (available == null && addToAvailable) availableChannels.Add(found);
return found;
}
/// <summary>
/// Joins a channel if it has not already been joined.
/// </summary>
/// <param name="channel">The channel to join.</param>
/// <param name="alreadyJoined">Whether the channel has already been joined server-side. Will skip a join request.</param>
/// <returns>The joined channel. Note that this may not match the parameter channel as it is a backed object.</returns>
public Channel JoinChannel(Channel channel, bool alreadyJoined = false)
{
if (channel == null) return null;
channel = getChannel(channel, addToJoined: true);
// ensure we are joined to the channel
if (!channel.Joined.Value)
{
if (alreadyJoined)
channel.Joined.Value = true;
else
{
switch (channel.Type)
{
case ChannelType.Public:
var req = new JoinChannelRequest(channel, api.LocalUser.Value);
req.Success += () => JoinChannel(channel, true);
req.Failure += ex => LeaveChannel(channel);
api.Queue(req);
return channel;
}
}
}
if (CurrentChannel.Value == null)
CurrentChannel.Value = channel;
if (!channel.MessagesLoaded)
{
// let's fetch a small number of messages to bring us up-to-date with the backlog.
fetchInitalMessages(channel);
}
return channel;
}
public void LeaveChannel(Channel channel)
{
if (channel == null) return;
if (channel == CurrentChannel.Value)
CurrentChannel.Value = null;
joinedChannels.Remove(channel);
if (channel.Joined.Value)
{
api.Queue(new LeaveChannelRequest(channel, api.LocalUser.Value));
channel.Joined.Value = false;
}
}
private long lastMessageId;
private bool channelsInitialised;
protected override Task Poll()
{
if (!api.IsLoggedIn)
return base.Poll();
var fetchReq = new GetUpdatesRequest(lastMessageId);
var tcs = new TaskCompletionSource<bool>();
fetchReq.Success += updates =>
{
if (updates?.Presence != null)
{
foreach (var channel in updates.Presence)
{
// we received this from the server so should mark the channel already joined.
JoinChannel(channel, true);
}
//todo: handle left channels
handleChannelMessages(updates.Messages);
foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
}
if (!channelsInitialised)
{
channelsInitialised = true;
// we want this to run after the first presence so we can see if the user is in any channels already.
initializeChannels();
}
tcs.SetResult(true);
};
fetchReq.Failure += _ => tcs.SetResult(false);
api.Queue(fetchReq);
return tcs.Task;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
this.api = api;
}
}
/// <summary>
/// An exception thrown when a channel could not been found.
/// </summary>
public class ChannelNotFoundException : Exception
{
public ChannelNotFoundException(string channelName)
: base($"A channel with the name {channelName} could not be found.")
{
}
}
}

View File

@ -1,11 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Chat
{
public enum ChannelType
{
Public,
Private,
Multiplayer,
Spectator,
Temporary,
PM,
Public
Group,
}
}

View File

@ -1,34 +1,33 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Cursor;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using OpenTK;
using osuTK;
namespace osu.Game.Online.Chat
{
/// <summary>
/// An invisible drawable that brings multiple <see cref="SpriteText"/> pieces together to form a consumable clickable link.
/// An invisible drawable that brings multiple <see cref="Drawable"/> pieces together to form a consumable clickable link.
/// </summary>
public class DrawableLinkCompiler : OsuHoverContainer, IHasTooltip
{
/// <summary>
/// Each word part of a chat link (split for word-wrap support).
/// </summary>
public List<SpriteText> Parts;
public List<Drawable> Parts;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(IEnumerable<SpriteText> parts)
public DrawableLinkCompiler(IEnumerable<Drawable> parts)
{
Parts = parts.ToList();
}
@ -45,9 +44,9 @@ namespace osu.Game.Online.Chat
private class LinkHoverSounds : HoverClickSounds
{
private readonly List<SpriteText> parts;
private readonly List<Drawable> parts;
public LinkHoverSounds(HoverSampleSet sampleSet, List<SpriteText> parts)
public LinkHoverSounds(HoverSampleSet sampleSet, List<Drawable> parts)
: base(sampleSet)
{
this.parts = parts;

View File

@ -1,11 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Chat
{
public class ErrorMessage : InfoMessage
{
public ErrorMessage(string message) : base(message)
public ErrorMessage(string message)
: base(message)
{
Sender.Colour = @"ff0000";
}

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
namespace osu.Game.Online.Chat
{
public class ExternalLinkOpener : Component
{
private GameHost host;
private DialogOverlay dialogOverlay;
private Bindable<bool> externalLinkWarning;
[BackgroundDependencyLoader(true)]
private void load(GameHost host, DialogOverlay dialogOverlay, OsuConfigManager config)
{
this.host = host;
this.dialogOverlay = dialogOverlay;
externalLinkWarning = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning);
}
public void OpenUrlExternally(string url)
{
if (externalLinkWarning.Value)
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url)));
else
host.OpenUrlExternally(url);
}
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Users;
@ -10,7 +10,8 @@ namespace osu.Game.Online.Chat
{
private static int infoID = -1;
public InfoMessage(string message) : base(infoID--)
public InfoMessage(string message)
: base(infoID--)
{
Timestamp = DateTimeOffset.Now;
Content = message;

View File

@ -1,11 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : LocalMessage
{
public LocalEchoMessage() : base(null)
public LocalEchoMessage()
: base(null)
{
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Chat
{

View File

@ -1,9 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using osu.Game.Users;
@ -16,10 +15,10 @@ namespace osu.Game.Online.Chat
//todo: this should be inside sender.
[JsonProperty(@"sender_id")]
public int UserId;
public long UserId;
[JsonProperty(@"channel_id")]
public int ChannelId;
public long ChannelId;
[JsonProperty(@"is_action")]
public bool IsAction;
@ -69,12 +68,4 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
}
public enum TargetType
{
[Description(@"channel")]
Channel,
[Description(@"user")]
User
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
@ -25,19 +25,19 @@ namespace osu.Game.Online.Chat
// This is in the format (<required>, [optional]):
// http[s]://<domain>.<tld>[:port][/path][?query][#fragment]
private static readonly Regex advanced_link_regex = new Regex(
// protocol
@"(?<link>[a-z]*?:\/\/" +
// domain + tld
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" +
// port (optional)
@"(?::\d+)?)" +
// path (optional)
@"(?<path>(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" +
// query (optional)
@"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" +
// fragment (optional)
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
RegexOptions.IgnoreCase);
// protocol
@"(?<link>[a-z]*?:\/\/" +
// domain + tld
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" +
// port (optional)
@"(?::\d+)?)" +
// path (optional)
@"(?<path>(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" +
// query (optional)
@"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" +
// fragment (optional)
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
RegexOptions.IgnoreCase);
// 00:00:000 (1,2,3) - test
private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*");
@ -51,19 +51,20 @@ namespace osu.Game.Online.Chat
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null)
{
int captureOffset = 0;
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index - captureOffset;
var displayText = string.Format(display,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
var linkText = string.Format(link,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
if (displayText.Length == 0 || linkText.Length == 0) continue;
@ -114,51 +115,63 @@ namespace osu.Game.Online.Chat
case "b":
case "beatmaps":
return new LinkDetails(LinkAction.OpenBeatmap, args[3]);
case "s":
case "beatmapsets":
case "d":
return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]);
case "u":
return new LinkDetails(LinkAction.OpenUserProfile, args[3]);
}
}
return new LinkDetails(LinkAction.External, null);
case "osu":
// every internal link also needs some kind of argument
if (args.Length < 3)
return new LinkDetails(LinkAction.External, null);
LinkAction linkType;
switch (args[1])
{
case "chan":
linkType = LinkAction.OpenChannel;
break;
case "edit":
linkType = LinkAction.OpenEditorTimestamp;
break;
case "b":
linkType = LinkAction.OpenBeatmap;
break;
case "s":
case "dl":
linkType = LinkAction.OpenBeatmapSet;
break;
case "spectate":
linkType = LinkAction.Spectate;
break;
case "u":
linkType = LinkAction.OpenUserProfile;
break;
default:
linkType = LinkAction.External;
break;
}
return new LinkDetails(linkType, args[2]);
case "osump":
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
default:
return new LinkDetails(LinkAction.External, null);
}

View File

@ -0,0 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat;
using osuTK.Graphics;
namespace osu.Game.Online.Chat
{
/// <summary>
/// Display a chat channel in an insolated region.
/// </summary>
public class StandAloneChatDisplay : CompositeDrawable
{
public readonly Bindable<Channel> Channel = new Bindable<Channel>();
public Action Exit;
private readonly FocusedTextBox textbox;
protected ChannelManager ChannelManager;
private DrawableChannel drawableChannel;
private readonly bool postingTextbox;
private const float textbox_height = 30;
/// <summary>
/// Construct a new instance.
/// </summary>
/// <param name="postingTextbox">Whether a textbox for posting new messages should be displayed.</param>
public StandAloneChatDisplay(bool postingTextbox = false)
{
this.postingTextbox = postingTextbox;
CornerRadius = 10;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
Colour = Color4.Black,
Alpha = 0.8f,
RelativeSizeAxes = Axes.Both
},
};
if (postingTextbox)
{
AddInternal(textbox = new FocusedTextBox
{
RelativeSizeAxes = Axes.X,
Height = textbox_height,
PlaceholderText = "type your message",
OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
textbox.Exit += () => Exit?.Invoke();
}
Channel.BindValueChanged(channelChanged);
}
[BackgroundDependencyLoader(true)]
private void load(ChannelManager manager)
{
if (ChannelManager == null)
ChannelManager = manager;
}
private void postMessage(TextBox sender, bool newtext)
{
var text = textbox.Text.Trim();
if (string.IsNullOrWhiteSpace(text))
return;
if (text[0] == '/')
ChannelManager?.PostCommand(text.Substring(1), Channel.Value);
else
ChannelManager?.PostMessage(text, target: Channel.Value);
textbox.Text = string.Empty;
}
public void Contract()
{
this.FadeIn(300);
this.MoveToY(0, 500, Easing.OutQuint);
}
public void Expand()
{
this.FadeOut(200);
this.MoveToY(100, 500, Easing.In);
}
protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message);
private void channelChanged(ValueChangedEvent<Channel> e)
{
drawableChannel?.Expire();
if (e.NewValue == null) return;
AddInternal(drawableChannel = new StandAloneDrawableChannel(e.NewValue)
{
CreateChatLineAction = CreateMessage,
Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 }
});
}
protected class StandAloneDrawableChannel : DrawableChannel
{
public Func<Message, ChatLine> CreateChatLineAction;
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 };
}
}
protected class StandAloneMessage : ChatLine
{
protected override float TextSize => 15;
protected override float HorizontalPadding => 10;
protected override float MessagePadding => 120;
public StandAloneMessage(Message message)
: base(message)
{
}
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Scoring;
namespace osu.Game.Online.Leaderboards
{
public class DrawableRank : Container
{
private readonly Sprite rankSprite;
private TextureStore textures;
public ScoreRank Rank { get; private set; }
public DrawableRank(ScoreRank rank)
{
Rank = rank;
Children = new Drawable[]
{
rankSprite = new Sprite
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fit
},
};
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
this.textures = textures;
updateTexture();
}
private void updateTexture()
{
string textureName;
switch (Rank)
{
default:
textureName = Rank.GetDescription();
break;
case ScoreRank.SH:
textureName = "SPlus";
break;
case ScoreRank.XH:
textureName = "SSPlus";
break;
}
rankSprite.Texture = textures.Get($@"Grades/{textureName}");
}
public void UpdateRank(ScoreRank newRank)
{
Rank = newRank;
if (LoadState >= LoadState.Ready)
updateTexture();
}
}
}

View File

@ -0,0 +1,326 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Online.Leaderboards
{
public abstract class Leaderboard<TScope, ScoreInfo> : Container, IOnlineComponent
{
private const double fade_duration = 300;
private readonly ScrollContainer scrollContainer;
private readonly Container placeholderContainer;
private FillFlowContainer<LeaderboardScore> scrollFlow;
private readonly LoadingAnimation loading;
private ScheduledDelegate showScoresDelegate;
private CancellationTokenSource showScoresCancellationSource;
private bool scoresLoadedOnce;
private IEnumerable<ScoreInfo> scores;
public IEnumerable<ScoreInfo> Scores
{
get => scores;
set
{
scores = value;
scoresLoadedOnce = true;
scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire();
scrollFlow = null;
loading.Hide();
// schedule because we may not be loaded yet (LoadComponentAsync complains).
showScoresDelegate?.Cancel();
showScoresCancellationSource?.Cancel();
if (scores == null || !scores.Any())
return;
// ensure placeholder is hidden when displaying scores
PlaceholderState = PlaceholderState.Successful;
scrollFlow = CreateScoreFlow();
scrollFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1));
if (!IsLoaded)
showScoresDelegate = Schedule(showScores);
else
showScores();
void showScores() => LoadComponentAsync(scrollFlow, _ =>
{
scrollContainer.Add(scrollFlow);
int i = 0;
foreach (var s in scrollFlow.Children)
{
using (s.BeginDelayedSequence(i++ * 50, true))
s.Show();
}
scrollContainer.ScrollTo(0f, false);
}, (showScoresCancellationSource = new CancellationTokenSource()).Token);
}
}
protected virtual FillFlowContainer<LeaderboardScore> CreateScoreFlow()
=> new FillFlowContainer<LeaderboardScore>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 5f),
Padding = new MarginPadding { Top = 10, Bottom = 5 },
};
private TScope scope;
public TScope Scope
{
get => scope;
set
{
if (value.Equals(scope))
return;
scope = value;
UpdateScores();
}
}
private PlaceholderState placeholderState;
/// <summary>
/// Update the placeholder visibility.
/// Setting this to anything other than PlaceholderState.Successful will cancel all existing retrieval requests and hide scores.
/// </summary>
protected PlaceholderState PlaceholderState
{
get => placeholderState;
set
{
if (value != PlaceholderState.Successful)
{
getScoresRequest?.Cancel();
getScoresRequest = null;
Scores = null;
}
if (value == placeholderState)
return;
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
replacePlaceholder(new RetrievalFailurePlaceholder
{
OnRetry = UpdateScores,
});
break;
case PlaceholderState.Unavailable:
replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"));
break;
case PlaceholderState.NoScores:
replacePlaceholder(new MessagePlaceholder(@"No records yet!"));
break;
case PlaceholderState.NotLoggedIn:
replacePlaceholder(new MessagePlaceholder(@"Please sign in to view online leaderboards!"));
break;
case PlaceholderState.NotSupporter:
replacePlaceholder(new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"));
break;
default:
replacePlaceholder(null);
break;
}
}
}
protected Leaderboard()
{
Children = new Drawable[]
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
},
loading = new LoadingAnimation(),
placeholderContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
};
}
private IAPIProvider api;
private ScheduledDelegate pendingUpdateScores;
[BackgroundDependencyLoader(true)]
private void load(IAPIProvider api)
{
this.api = api;
api?.Register(this);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
api?.Unregister(this);
}
public void RefreshScores() => UpdateScores();
private APIRequest getScoresRequest;
public void APIStateChanged(IAPIProvider api, APIState state)
{
if (state == APIState.Online)
UpdateScores();
}
protected void UpdateScores()
{
// don't display any scores or placeholder until the first Scores_Set has been called.
// this avoids scope changes flickering a "no scores" placeholder before initialisation of song select is finished.
if (!scoresLoadedOnce) return;
getScoresRequest?.Cancel();
getScoresRequest = null;
pendingUpdateScores?.Cancel();
pendingUpdateScores = Schedule(() =>
{
PlaceholderState = PlaceholderState.Retrieving;
loading.Show();
getScoresRequest = FetchScores(scores => Schedule(() =>
{
Scores = scores;
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
}));
if (getScoresRequest == null)
return;
if (api?.IsLoggedIn != true)
{
PlaceholderState = PlaceholderState.NotLoggedIn;
return;
}
getScoresRequest.Failure += e => Schedule(() =>
{
if (e is OperationCanceledException)
return;
PlaceholderState = PlaceholderState.NetworkFailure;
});
api.Queue(getScoresRequest);
});
}
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected abstract APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback);
private Placeholder currentPlaceholder;
private void replacePlaceholder(Placeholder placeholder)
{
if (placeholder != null && placeholder.Equals(currentPlaceholder))
return;
currentPlaceholder?.FadeOut(150, Easing.OutQuint).Expire();
if (placeholder == null)
{
currentPlaceholder = null;
return;
}
placeholderContainer.Child = placeholder;
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, fade_duration * 3, Easing.OutQuint);
placeholder.FadeInFromZero(fade_duration, Easing.OutQuint);
currentPlaceholder = placeholder;
}
protected virtual bool FadeBottom => true;
protected virtual bool FadeTop => false;
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
var fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight;
var fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT;
if (!scrollContainer.IsScrolledToEnd())
fadeBottom -= LeaderboardScore.HEIGHT;
if (scrollFlow == null)
return;
foreach (var c in scrollFlow.Children)
{
var topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y;
var bottomY = topY + LeaderboardScore.HEIGHT;
bool requireTopFade = FadeTop && topY <= fadeTop;
bool requireBottomFade = FadeBottom && bottomY >= fadeBottom;
if (!requireTopFade && !requireBottomFade)
c.Colour = Color4.White;
else if (topY > fadeBottom + LeaderboardScore.HEIGHT || bottomY < fadeTop - LeaderboardScore.HEIGHT)
c.Colour = Color4.Transparent;
else
{
if (bottomY - fadeBottom > 0 && FadeBottom)
c.Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / LeaderboardScore.HEIGHT, 1)),
Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / LeaderboardScore.HEIGHT, 1)));
else if (FadeTop)
c.Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / LeaderboardScore.HEIGHT, 1)),
Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / LeaderboardScore.HEIGHT, 1)));
}
}
}
protected abstract LeaderboardScore CreateDrawableScore(ScoreInfo model, int index);
}
}

View File

@ -0,0 +1,394 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Online.Leaderboards
{
public class LeaderboardScore : OsuClickableContainer
{
public readonly int RankPosition;
public const float HEIGHT = 60;
private const float corner_radius = 5;
private const float edge_margin = 5;
private const float background_alpha = 0.25f;
private const float rank_width = 30;
protected Container RankContainer { get; private set; }
private readonly ScoreInfo score;
private Box background;
private Container content;
private Drawable avatar;
private Drawable scoreRank;
private OsuSpriteText nameLabel;
private GlowingSpriteText scoreLabel;
private Container flagBadgeContainer;
private FillFlowContainer<ModIcon> modsContainer;
private List<ScoreComponentLabel> statisticsLabels;
public LeaderboardScore(ScoreInfo score, int rank)
{
this.score = score;
RankPosition = rank;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
}
[BackgroundDependencyLoader]
private void load()
{
var user = score.User;
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList();
Avatar innerAvatar;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
Width = rank_width,
Children = new[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 22, italics: true),
Text = RankPosition.ToString(),
},
},
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = rank_width, },
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = corner_radius,
Masking = true,
Children = new[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = background_alpha,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(edge_margin),
Children = new[]
{
avatar = new DelayedLoadWrapper(
innerAvatar = new Avatar(user)
{
RelativeSizeAxes = Axes.Both,
CornerRadius = corner_radius,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 1,
Colour = Color4.Black.Opacity(0.2f),
},
})
{
RelativeSizeAxes = Axes.None,
Size = new Vector2(HEIGHT - edge_margin * 2, HEIGHT - edge_margin * 2),
},
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Position = new Vector2(HEIGHT - edge_margin, 0f),
Children = new Drawable[]
{
nameLabel = new OsuSpriteText
{
Text = user.Username,
Font = OsuFont.GetFont(size: 23, weight: FontWeight.Bold, italics: true)
},
new FillFlowContainer
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Children = new Drawable[]
{
flagBadgeContainer = new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Size = new Vector2(87f, 20f),
Masking = true,
Children = new Drawable[]
{
new DrawableFlag(user.Country)
{
Width = 30,
RelativeSizeAxes = Axes.Y,
},
},
},
new FillFlowContainer
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
},
},
},
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
scoreLabel = new GlowingSpriteText(score.TotalScore.ToString(@"N0"), OsuFont.Numeric.With(size: 23), Color4.White, OsuColour.FromHex(@"83ccfa")),
RankContainer = new Container
{
Size = new Vector2(40f, 20f),
Children = new[]
{
scoreRank = new DrawableRank(score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40f)
},
},
},
},
},
modsContainer = new FillFlowContainer<ModIcon>
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
},
},
},
},
},
};
innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200);
}
public override void Show()
{
foreach (var d in new[] { avatar, nameLabel, scoreLabel, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels))
d.FadeOut();
Alpha = 0;
content.MoveToY(75);
avatar.MoveToX(75);
nameLabel.MoveToX(150);
this.FadeIn(200);
content.MoveToY(0, 800, Easing.OutQuint);
using (BeginDelayedSequence(100, true))
{
avatar.FadeIn(300, Easing.OutQuint);
nameLabel.FadeIn(350, Easing.OutQuint);
avatar.MoveToX(0, 300, Easing.OutQuint);
nameLabel.MoveToX(0, 350, Easing.OutQuint);
using (BeginDelayedSequence(250, true))
{
scoreLabel.FadeIn(200);
scoreRank.FadeIn(200);
using (BeginDelayedSequence(50, true))
{
var drawables = new Drawable[] { flagBadgeContainer, modsContainer }.Concat(statisticsLabels).ToArray();
for (int i = 0; i < drawables.Length; i++)
drawables[i].FadeIn(100 + i * 50);
}
}
}
}
protected virtual IEnumerable<LeaderboardScoreStatistic> GetStatistics(ScoreInfo model) => new[]
{
new LeaderboardScoreStatistic(FontAwesome.Solid.Link, "Max Combo", model.MaxCombo.ToString()),
new LeaderboardScoreStatistic(FontAwesome.Solid.Crosshairs, "Accuracy", string.Format(model.Accuracy % 1 == 0 ? @"{0:P0}" : @"{0:P2}", model.Accuracy))
};
protected override bool OnHover(HoverEvent e)
{
background.FadeTo(0.5f, 300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeTo(background_alpha, 200, Easing.OutQuint);
base.OnHoverLost(e);
}
private class GlowingSpriteText : Container
{
public GlowingSpriteText(string text, FontUsage font, Color4 textColour, Color4 glowColour)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new BufferedContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BlurSigma = new Vector2(4),
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
Blending = BlendingMode.Additive,
Size = new Vector2(3f),
Children = new[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = font.With(fixedWidth: true),
Text = text,
Colour = glowColour,
Shadow = false,
},
},
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = font.With(fixedWidth: true),
Text = text,
Colour = textColour,
Shadow = false,
},
};
}
}
private class ScoreComponentLabel : Container, IHasTooltip
{
private const float icon_size = 20;
private readonly string name;
private readonly FillFlowContainer content;
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
public string TooltipText => name;
public ScoreComponentLabel(LeaderboardScoreStatistic statistic)
{
name = statistic.Name;
AutoSizeAxes = Axes.Both;
Child = content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(icon_size),
Rotation = 45,
Colour = OsuColour.FromHex(@"3087ac"),
Icon = FontAwesome.Solid.Square,
Shadow = true,
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(icon_size - 6),
Colour = OsuColour.FromHex(@"a4edff"),
Icon = statistic.Icon,
},
},
},
new GlowingSpriteText(statistic.Value, OsuFont.GetFont(size: 17, weight: FontWeight.Bold), Color4.White, OsuColour.FromHex(@"83ccfa"))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
},
};
}
}
public class LeaderboardScoreStatistic
{
public IconUsage Icon;
public string Value;
public string Name;
public LeaderboardScoreStatistic(IconUsage icon, string name, string value)
{
Icon = icon;
Name = name;
Value = value;
}
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Online.Leaderboards
{
public class MessagePlaceholder : Placeholder
{
private readonly string message;
public MessagePlaceholder(string message)
{
AddIcon(FontAwesome.Solid.ExclamationCircle, cp =>
{
cp.Font = cp.Font.With(size: TEXT_SIZE);
cp.Padding = new MarginPadding { Right = 10 };
});
AddText(this.message = message);
}
public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message;
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Online.Leaderboards
{
public abstract class Placeholder : OsuTextFlowContainer, IEquatable<Placeholder>
{
protected const float TEXT_SIZE = 22;
protected Placeholder()
: base(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
TextAnchor = Anchor.TopCentre;
Padding = new MarginPadding(20);
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
}
public virtual bool Equals(Placeholder other) => GetType() == other?.GetType();
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Leaderboards
{
public enum PlaceholderState
{
Successful,
Retrieving,
NetworkFailure,
Unavailable,
NoScores,
NotLoggedIn,
NotSupporter,
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Online.Leaderboards
{
public class RetrievalFailurePlaceholder : Placeholder
{
public Action OnRetry;
public RetrievalFailurePlaceholder()
{
AddArbitraryDrawable(new RetryButton
{
Action = () => OnRetry?.Invoke(),
Padding = new MarginPadding { Right = 10 }
});
AddText(@"Couldn't retrieve scores!");
}
public class RetryButton : OsuHoverContainer
{
private readonly SpriteIcon icon;
public new Action Action;
public RetryButton()
{
AutoSizeAxes = Axes.Both;
Child = new OsuClickableContainer
{
AutoSizeAxes = Axes.Both,
Action = () => Action?.Invoke(),
Child = icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(TEXT_SIZE),
Shadow = true,
},
};
}
protected override bool OnMouseDown(MouseDownEvent e)
{
icon.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override bool OnMouseUp(MouseUpEvent e)
{
icon.ScaleTo(1, 1000, Easing.OutElastic);
return base.OnMouseUp(e);
}
}
}
}

View File

@ -1,11 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
namespace osu.Game.Online.Multiplayer
@ -13,134 +9,10 @@ namespace osu.Game.Online.Multiplayer
public abstract class GameType
{
public abstract string Name { get; }
public abstract Drawable GetIcon(OsuColour colours, float size);
}
public class GameTypeTag : GameType
{
public override string Name => "Tag";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.fa_refresh,
Size = new Vector2(size),
Colour = colours.Blue,
Shadow = false,
};
}
}
public class GameTypeVersus : GameType
{
public override string Name => "Versus";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new VersusRow(colours.Blue, colours.Blue, size * 0.6f)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
}
public class GameTypeTagTeam : GameType
{
public override string Name => "Tag Team";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f),
Children = new[]
{
new SpriteIcon
{
Icon = FontAwesome.fa_refresh,
Size = new Vector2(size * 0.75f),
Colour = colours.Blue,
Shadow = false,
},
new SpriteIcon
{
Icon = FontAwesome.fa_refresh,
Size = new Vector2(size * 0.75f),
Colour = colours.Pink,
Shadow = false,
},
},
};
}
}
public class GameTypeTeamVersus : GameType
{
public override string Name => "Team Versus";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2f),
Children = new[]
{
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
},
};
}
}
public class VersusRow : FillFlowContainer
{
public VersusRow(Color4 first, Color4 second, float size)
{
var triangleSize = new Vector2(size);
AutoSizeAxes = Axes.Both;
Spacing = new Vector2(2f, 0f);
Children = new[]
{
new Container
{
Size = triangleSize,
Colour = first,
Children = new[]
{
new EquilateralTriangle
{
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = 90,
EdgeSmoothness = new Vector2(1f),
},
},
},
new Container
{
Size = triangleSize,
Colour = second,
Children = new[]
{
new EquilateralTriangle
{
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = -90,
EdgeSmoothness = new Vector2(1f),
},
},
},
};
}
public override int GetHashCode() => GetType().GetHashCode();
public override bool Equals(object obj) => GetType() == obj?.GetType();
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class GameTypeTag : GameType
{
public override string Name => "Tag";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(size),
Colour = colours.Blue,
Shadow = false,
};
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class GameTypeTagTeam : GameType
{
public override string Name => "Tag Team";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f),
Children = new[]
{
new SpriteIcon
{
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(size * 0.75f),
Colour = colours.Blue,
Shadow = false,
},
new SpriteIcon
{
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(size * 0.75f),
Colour = colours.Pink,
Shadow = false,
},
},
};
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class GameTypeTeamVersus : GameType
{
public override string Name => "Team Versus";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2f),
Children = new[]
{
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
},
};
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class GameTypeTimeshift : GameType
{
public override string Name => "Timeshift";
public override Drawable GetIcon(OsuColour colours, float size) => new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Regular.Clock,
Size = new Vector2(size),
Colour = colours.Blue,
Shadow = false
};
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Graphics;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class GameTypeVersus : GameType
{
public override string Name => "Versus";
public override Drawable GetIcon(OsuColour colours, float size)
{
return new VersusRow(colours.Blue, colours.Blue, size * 0.6f)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Online.Multiplayer.GameTypes
{
public class VersusRow : FillFlowContainer
{
public VersusRow(Color4 first, Color4 second, float size)
{
var triangleSize = new Vector2(size);
AutoSizeAxes = Axes.Both;
Spacing = new Vector2(2f, 0f);
Children = new[]
{
new Container
{
Size = triangleSize,
Colour = first,
Children = new[]
{
new EquilateralTriangle
{
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = 90,
EdgeSmoothness = new Vector2(1f),
},
},
},
new Container
{
Size = triangleSize,
Colour = second,
Children = new[]
{
new EquilateralTriangle
{
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = -90,
EdgeSmoothness = new Vector2(1f),
},
},
},
};
}
}
}

View File

@ -0,0 +1,93 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.Multiplayer
{
public class PlaylistItem
{
[JsonProperty("id")]
public int ID { get; set; }
[JsonProperty("beatmap_id")]
public int BeatmapID { get; set; }
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonIgnore]
public BeatmapInfo Beatmap
{
get => beatmap;
set
{
beatmap = value;
BeatmapID = value?.OnlineBeatmapID ?? 0;
}
}
[JsonIgnore]
public RulesetInfo Ruleset { get; set; }
[JsonIgnore]
public readonly List<Mod> AllowedMods = new List<Mod>();
[JsonIgnore]
public readonly List<Mod> RequiredMods = new List<Mod>();
[JsonProperty("beatmap")]
private APIBeatmap apiBeatmap { get; set; }
[JsonProperty("allowed_mods")]
private APIMod[] allowedMods
{
get => AllowedMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray();
set => _allowedMods = value;
}
[JsonProperty("required_mods")]
private APIMod[] requiredMods
{
get => RequiredMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray();
set => _requiredMods = value;
}
private BeatmapInfo beatmap;
private APIMod[] _allowedMods;
private APIMod[] _requiredMods;
public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets)
{
// If we don't have an api beatmap, the request occurred as a result of room creation, so we can query the local beatmap instead
// Todo: Is this a bug? Room creation only returns the beatmap ID
Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets);
Ruleset = rulesets.GetRuleset(RulesetID);
if (_allowedMods != null)
{
AllowedMods.Clear();
AllowedMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _allowedMods.Any(m => m.Acronym == mod.Acronym)));
_allowedMods = null;
}
if (_requiredMods != null)
{
RequiredMods.Clear();
RequiredMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => _requiredMods.Any(m => m.Acronym == mod.Acronym)));
_requiredMods = null;
}
}
public bool ShouldSerializeID() => false;
public bool ShouldSerializeapiBeatmap() => false;
}
}

View File

@ -1,22 +1,152 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Configuration;
using osu.Game.Beatmaps;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Multiplayer.GameTypes;
using osu.Game.Online.Multiplayer.RoomStatuses;
using osu.Game.Users;
namespace osu.Game.Online.Multiplayer
{
public class Room
{
public Bindable<string> Name = new Bindable<string>();
public Bindable<User> Host = new Bindable<User>();
public Bindable<RoomStatus> Status = new Bindable<RoomStatus>();
public Bindable<RoomAvailability> Availability = new Bindable<RoomAvailability>();
public Bindable<GameType> Type = new Bindable<GameType>();
public Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
public Bindable<int?> MaxParticipants = new Bindable<int?>();
public Bindable<IEnumerable<User>> Participants = new Bindable<IEnumerable<User>>();
[Cached]
[JsonProperty("id")]
public Bindable<int?> RoomID { get; private set; } = new Bindable<int?>();
[Cached]
[JsonProperty("name")]
public Bindable<string> Name { get; private set; } = new Bindable<string>();
[Cached]
[JsonProperty("host")]
public Bindable<User> Host { get; private set; } = new Bindable<User>();
[Cached]
[JsonProperty("playlist")]
public BindableList<PlaylistItem> Playlist { get; private set; } = new BindableList<PlaylistItem>();
[Cached]
[JsonIgnore]
public Bindable<PlaylistItem> CurrentItem { get; private set; } = new Bindable<PlaylistItem>();
[Cached]
[JsonProperty("channel_id")]
public Bindable<int> ChannelId { get; private set; } = new Bindable<int>();
[Cached]
[JsonIgnore]
public Bindable<TimeSpan> Duration { get; private set; } = new Bindable<TimeSpan>(TimeSpan.FromMinutes(30));
[Cached]
[JsonIgnore]
public Bindable<int?> MaxAttempts { get; private set; } = new Bindable<int?>();
[Cached]
[JsonIgnore]
public Bindable<RoomStatus> Status { get; private set; } = new Bindable<RoomStatus>(new RoomStatusOpen());
[Cached]
[JsonIgnore]
public Bindable<RoomAvailability> Availability { get; private set; } = new Bindable<RoomAvailability>();
[Cached]
[JsonIgnore]
public Bindable<GameType> Type { get; private set; } = new Bindable<GameType>(new GameTypeTimeshift());
[Cached]
[JsonIgnore]
public Bindable<int?> MaxParticipants { get; private set; } = new Bindable<int?>();
[Cached]
[JsonIgnore]
public Bindable<IEnumerable<User>> Participants { get; private set; } = new Bindable<IEnumerable<User>>(Enumerable.Empty<User>());
[Cached]
public Bindable<int> ParticipantCount { get; private set; } = new Bindable<int>();
public Room()
{
Playlist.ItemsAdded += updateCurrent;
Playlist.ItemsRemoved += updateCurrent;
updateCurrent(Playlist);
}
private void updateCurrent(IEnumerable<PlaylistItem> playlist)
{
CurrentItem.Value = playlist.FirstOrDefault();
}
// todo: TEMPORARY
[JsonProperty("participant_count")]
private int? participantCount
{
get => ParticipantCount.Value;
set => ParticipantCount.Value = value ?? 0;
}
[JsonProperty("duration")]
private int duration
{
get => (int)Duration.Value.TotalMinutes;
set => Duration.Value = TimeSpan.FromMinutes(value);
}
// Only supports retrieval for now
[Cached]
[JsonProperty("ends_at")]
public Bindable<DateTimeOffset> EndDate { get; private set; } = new Bindable<DateTimeOffset>();
// Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930)
[JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)]
private int? maxAttempts
{
get => MaxAttempts.Value;
set => MaxAttempts.Value = value;
}
/// <summary>
/// The position of this <see cref="Room"/> in the list. This is not read from or written to the API.
/// </summary>
[JsonIgnore]
public Bindable<int> Position { get; private set; } = new Bindable<int>(-1);
public void CopyFrom(Room other)
{
RoomID.Value = other.RoomID.Value;
Name.Value = other.Name.Value;
if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id)
Host.Value = other.Host.Value;
ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value;
Type.Value = other.Type.Value;
MaxParticipants.Value = other.MaxParticipants.Value;
ParticipantCount.Value = other.ParticipantCount.Value;
Participants.Value = other.Participants.Value.ToArray();
EndDate.Value = other.EndDate.Value;
if (DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded();
// Todo: Temporary, should only remove/add new items (requires framework changes)
if (Playlist.Count == 0)
Playlist.AddRange(other.Playlist);
else if (other.Playlist.Count > 0)
Playlist.First().ID = other.Playlist.First().ID;
Position = other.Position;
}
public bool ShouldSerializeRoomID() => false;
public bool ShouldSerializeHost() => false;
public bool ShouldSerializeEndDate() => false;
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;

View File

@ -1,7 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using OpenTK.Graphics;
using osuTK.Graphics;
using osu.Game.Graphics;
namespace osu.Game.Online.Multiplayer
@ -10,17 +10,8 @@ namespace osu.Game.Online.Multiplayer
{
public abstract string Message { get; }
public abstract Color4 GetAppropriateColour(OsuColour colours);
}
public class RoomStatusOpen : RoomStatus
{
public override string Message => @"Welcoming Players";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
}
public class RoomStatusPlaying : RoomStatus
{
public override string Message => @"Now Playing";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
public override int GetHashCode() => GetType().GetHashCode();
public override bool Equals(object obj) => GetType() == obj?.GetType();
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Multiplayer.RoomStatuses
{
public class RoomStatusEnded : RoomStatus
{
public override string Message => @"Ended";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker;
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Multiplayer.RoomStatuses
{
public class RoomStatusOpen : RoomStatus
{
public override string Message => @"Welcoming Players";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Multiplayer.RoomStatuses
{
public class RoomStatusPlaying : RoomStatus
{
public override string Message => @"Now Playing";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
}
}

View File

@ -0,0 +1,127 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading.Tasks;
using osu.Framework.Graphics;
using osu.Framework.Threading;
namespace osu.Game.Online
{
/// <summary>
/// A component which requires a constant polling process.
/// </summary>
public abstract class PollingComponent : Component
{
private double? lastTimePolled;
private ScheduledDelegate scheduledPoll;
private bool pollingActive;
private double timeBetweenPolls;
/// <summary>
/// The time in milliseconds to wait between polls.
/// Setting to zero stops all polling.
/// </summary>
public double TimeBetweenPolls
{
get => timeBetweenPolls;
set
{
timeBetweenPolls = value;
scheduledPoll?.Cancel();
pollIfNecessary();
}
}
/// <summary>
///
/// </summary>
/// <param name="timeBetweenPolls">The initial time in milliseconds to wait between polls. Setting to zero stops all polling.</param>
protected PollingComponent(double timeBetweenPolls = 0)
{
TimeBetweenPolls = timeBetweenPolls;
}
protected override void LoadComplete()
{
base.LoadComplete();
pollIfNecessary();
}
private bool pollIfNecessary()
{
// we must be loaded so we have access to clock.
if (!IsLoaded) return false;
// there's already a poll process running.
if (pollingActive) return false;
// don't try polling if the time between polls hasn't been set.
if (timeBetweenPolls == 0) return false;
if (!lastTimePolled.HasValue)
{
doPoll();
return true;
}
if (Time.Current - lastTimePolled.Value > timeBetweenPolls)
{
doPoll();
return true;
}
// not ennough time has passed since the last poll. we do want to schedule a poll to happen, though.
scheduleNextPoll();
return false;
}
private void doPoll()
{
scheduledPoll = null;
pollingActive = true;
Poll().ContinueWith(_ => pollComplete());
}
/// <summary>
/// Performs a poll. Implement but do not call this.
/// </summary>
protected virtual Task Poll()
{
return Task.CompletedTask;
}
/// <summary>
/// Immediately performs a <see cref="Poll"/>.
/// </summary>
public void PollImmediately()
{
lastTimePolled = Time.Current - timeBetweenPolls;
scheduleNextPoll();
}
/// <summary>
/// Call when a poll operation has completed.
/// </summary>
private void pollComplete()
{
lastTimePolled = Time.Current;
pollingActive = false;
if (scheduledPoll == null)
pollIfNecessary();
}
private void scheduleNextPoll()
{
scheduledPoll?.Cancel();
double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0;
scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration));
}
}
}