mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' into changelog-overlay
This commit is contained in:
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
28
osu.Game/Online/API/APIMessagesRequest.cs
Normal file
28
osu.Game/Online/API/APIMessagesRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
41
osu.Game/Online/API/RegistrationRequest.cs
Normal file
41
osu.Game/Online/API/RegistrationRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
35
osu.Game/Online/API/Requests/CreateRoomRequest.cs
Normal file
35
osu.Game/Online/API/Requests/CreateRoomRequest.cs
Normal 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";
|
||||
}
|
||||
}
|
30
osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
Normal file
30
osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -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" : "")}";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
20
osu.Game/Online/API/Requests/GetRoomScoresRequest.cs
Normal file
20
osu.Game/Online/API/Requests/GetRoomScoresRequest.cs
Normal 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";
|
||||
}
|
||||
}
|
47
osu.Game/Online/API/Requests/GetRoomsRequest.cs
Normal file
47
osu.Game/Online/API/Requests/GetRoomsRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
31
osu.Game/Online/API/Requests/JoinRoomRequest.cs
Normal file
31
osu.Game/Online/API/Requests/JoinRoomRequest.cs
Normal 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}";
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
31
osu.Game/Online/API/Requests/PartRoomRequest.cs
Normal file
31
osu.Game/Online/API/Requests/PartRoomRequest.cs
Normal 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}";
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
14
osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs
Normal file
14
osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
osu.Game/Online/API/Requests/Responses/APILegacyScores.cs
Normal file
14
osu.Game/Online/API/Requests/Responses/APILegacyScores.cs
Normal 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;
|
||||
}
|
||||
}
|
14
osu.Game/Online/API/Requests/Responses/APIMod.cs
Normal file
14
osu.Game/Online/API/Requests/Responses/APIMod.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
17
osu.Game/Online/API/Requests/Responses/APIRoomScoreInfo.cs
Normal file
17
osu.Game/Online/API/Requests/Responses/APIRoomScoreInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
13
osu.Game/Online/API/Requests/Responses/APIScoreToken.cs
Normal file
13
osu.Game/Online/API/Requests/Responses/APIScoreToken.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
}
|
||||
|
20
osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs
Normal file
20
osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
40
osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
Normal file
40
osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
Normal 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}";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
443
osu.Game/Online/Chat/ChannelManager.cs
Normal file
443
osu.Game/Online/Chat/ChannelManager.cs
Normal 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.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
}
|
||||
|
36
osu.Game/Online/Chat/ExternalLinkOpener.cs
Normal file
36
osu.Game/Online/Chat/ExternalLinkOpener.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
151
osu.Game/Online/Chat/StandAloneChatDisplay.cs
Normal file
151
osu.Game/Online/Chat/StandAloneChatDisplay.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
osu.Game/Online/Leaderboards/DrawableRank.cs
Normal file
74
osu.Game/Online/Leaderboards/DrawableRank.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
326
osu.Game/Online/Leaderboards/Leaderboard.cs
Normal file
326
osu.Game/Online/Leaderboards/Leaderboard.cs
Normal 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);
|
||||
}
|
||||
}
|
394
osu.Game/Online/Leaderboards/LeaderboardScore.cs
Normal file
394
osu.Game/Online/Leaderboards/LeaderboardScore.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
osu.Game/Online/Leaderboards/MessagePlaceholder.cs
Normal file
26
osu.Game/Online/Leaderboards/MessagePlaceholder.cs
Normal 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;
|
||||
}
|
||||
}
|
29
osu.Game/Online/Leaderboards/Placeholder.cs
Normal file
29
osu.Game/Online/Leaderboards/Placeholder.cs
Normal 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();
|
||||
}
|
||||
}
|
16
osu.Game/Online/Leaderboards/PlaceholderState.cs
Normal file
16
osu.Game/Online/Leaderboards/PlaceholderState.cs
Normal 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,
|
||||
}
|
||||
}
|
64
osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs
Normal file
64
osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
28
osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs
Normal file
28
osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
45
osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs
Normal file
45
osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
32
osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs
Normal file
32
osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs
Normal 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),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
25
osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs
Normal file
25
osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
22
osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs
Normal file
22
osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
55
osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs
Normal file
55
osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs
Normal 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),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
93
osu.Game/Online/Multiplayer/PlaylistItem.cs
Normal file
93
osu.Game/Online/Multiplayer/PlaylistItem.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
14
osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs
Normal file
14
osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs
Normal 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;
|
||||
}
|
||||
}
|
14
osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs
Normal file
14
osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
127
osu.Game/Online/PollingComponent.cs
Normal file
127
osu.Game/Online/PollingComponent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user