Merge remote-tracking branch 'origin/master' into Private_Messages

This commit is contained in:
miterosan
2018-04-14 13:31:03 +02:00
1075 changed files with 94947 additions and 94933 deletions

View File

@ -1,339 +1,334 @@
// 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public class APIAccess : Component, IAPIProvider
{
private readonly OsuConfigManager config;
private readonly OAuth authentication;
public string Endpoint = @"https://osu.ppy.sh";
private const string client_id = @"5";
private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
private ConcurrentQueue<APIRequest> queue = new ConcurrentQueue<APIRequest>();
/// <summary>
/// The username/email provided by the user when initiating a login.
/// </summary>
public string ProvidedUsername { get; private set; }
private string password;
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
public string Token
{
get { return authentication.Token?.ToString(); }
set { authentication.Token = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); }
}
protected bool HasLogin => Token != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password);
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
public APIAccess(OsuConfigManager config)
{
this.config = config;
authentication = new OAuth(client_id, client_secret, Endpoint);
log = Logger.GetLogger(LoggingTarget.Network);
ProvidedUsername = config.Get<string>(OsuSetting.Username);
Token = config.Get<string>(OsuSetting.Token);
Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
internal new void Schedule(Action action) => base.Schedule(action);
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 string AccessToken => authentication.RequestAccessToken();
/// <summary>
/// Number of consecutive requests which failed due to network issues.
/// </summary>
private int failureCount;
private void run()
{
while (!cancellationToken.IsCancellationRequested)
{
switch (State)
{
case APIState.Failing:
//todo: replace this with a ping request.
log.Add(@"In a failing state, waiting a bit before we try again...");
Thread.Sleep(5000);
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new ListChannelsRequest { Timeout = 5000 });
}
break;
case APIState.Offline:
case APIState.Connecting:
//work to restore a connection...
if (!HasLogin)
{
State = APIState.Offline;
Thread.Sleep(50);
continue;
}
State = APIState.Connecting;
// save the username at this point, if the user requested for it to be.
config.Set(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password))
{
//todo: this fails even on network-related issues. we should probably handle those differently.
//NotificationOverlay.ShowMessage("Login failed!");
log.Add(@"Login failed!");
password = null;
authentication.Clear();
continue;
}
var userReq = new GetUserRequest();
userReq.Success += u =>
{
LocalUser.Value = u;
failureCount = 0;
//we're connected!
State = APIState.Online;
};
if (!handleRequest(userReq))
{
Thread.Sleep(500);
continue;
}
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while (State != APIState.Online)
Thread.Sleep(500);
break;
}
//hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout(false);
State = APIState.Offline;
continue;
}
//process the request queue.
APIRequest req;
while (queue.TryPeek(out req))
{
if (handleRequest(req))
{
//we have succeeded, so let's unqueue.
queue.TryDequeue(out req);
}
}
Thread.Sleep(1);
}
}
public void Login(string username, string password)
{
Debug.Assert(State == APIState.Offline);
ProvidedUsername = username;
this.password = password;
}
/// <summary>
/// Handle a single API request.
/// </summary>
/// <param name="req">The request.</param>
/// <returns>true if we should remove this request from the queue.</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;
failureCount = 0;
return true;
}
catch (WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
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;
}
catch (Exception e)
{
if (e is TimeoutException)
log.Add(@"API level timeout exception was hit");
req.Fail(e);
return true;
}
}
private APIState state;
public APIState State
{
get { return state; }
private set
{
APIState oldState = state;
APIState newState = value;
state = value;
if (oldState != newState)
{
log.Add($@"We just went {newState}!");
Scheduler.Add(delegate
{
components.ForEach(c => c.APIStateChanged(this, newState));
OnStateChange?.Invoke(oldState, newState);
});
}
}
}
public bool IsLoggedIn => LocalUser.Value.Id > 1;
public void Queue(APIRequest request) => queue.Enqueue(request);
public event StateChangeDelegate OnStateChange;
public delegate void StateChangeDelegate(APIState oldState, APIState newState);
private void flushQueue(bool failOldRequests = true)
{
var oldQueue = queue;
//flush the queue.
queue = new ConcurrentQueue<APIRequest>();
if (failOldRequests)
{
APIRequest req;
while (oldQueue.TryDequeue(out req))
req.Fail(new WebException(@"Disconnected from server"));
}
}
public void Logout(bool clearUsername = true)
{
flushQueue();
if (clearUsername) ProvidedUsername = null;
password = null;
authentication.Clear();
LocalUser.Value = createGuestUser();
}
private static User createGuestUser() => new User
{
Username = @"Guest",
Id = 1,
};
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? Token : string.Empty);
config.Save();
flushQueue();
cancellationToken.Cancel();
}
}
public enum APIState
{
/// <summary>
/// We cannot login (not enough credentials).
/// </summary>
Offline,
/// <summary>
/// We are having connectivity issues.
/// </summary>
Failing,
/// <summary>
/// We are in the process of (re-)connecting.
/// </summary>
Connecting,
/// <summary>
/// We are online.
/// </summary>
Online
}
}
// 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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public class APIAccess : Component, IAPIProvider
{
private readonly OsuConfigManager config;
private readonly OAuth authentication;
public string Endpoint = @"https://osu.ppy.sh";
private const string client_id = @"5";
private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
private ConcurrentQueue<APIRequest> queue = new ConcurrentQueue<APIRequest>();
/// <summary>
/// The username/email provided by the user when initiating a login.
/// </summary>
public string ProvidedUsername { get; private set; }
private string password;
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password);
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
public APIAccess(OsuConfigManager config)
{
this.config = config;
authentication = new OAuth(client_id, client_secret, Endpoint);
log = Logger.GetLogger(LoggingTarget.Network);
ProvidedUsername = config.Get<string>(OsuSetting.Username);
authentication.TokenString = config.Get<string>(OsuSetting.Token);
authentication.Token.ValueChanged += onTokenChanged;
Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
private void onTokenChanged(OAuthToken token) => 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);
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 string AccessToken => authentication.RequestAccessToken();
/// <summary>
/// Number of consecutive requests which failed due to network issues.
/// </summary>
private int failureCount;
private void run()
{
while (!cancellationToken.IsCancellationRequested)
{
switch (State)
{
case APIState.Failing:
//todo: replace this with a ping request.
log.Add(@"In a failing state, waiting a bit before we try again...");
Thread.Sleep(5000);
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new ListChannelsRequest { Timeout = 5000 });
}
break;
case APIState.Offline:
case APIState.Connecting:
//work to restore a connection...
if (!HasLogin)
{
State = APIState.Offline;
Thread.Sleep(50);
continue;
}
State = APIState.Connecting;
// save the username at this point, if the user requested for it to be.
config.Set(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password))
{
//todo: this fails even on network-related issues. we should probably handle those differently.
//NotificationOverlay.ShowMessage("Login failed!");
log.Add(@"Login failed!");
password = null;
authentication.Clear();
continue;
}
var userReq = new GetUserRequest();
userReq.Success += u =>
{
LocalUser.Value = u;
failureCount = 0;
//we're connected!
State = APIState.Online;
};
if (!handleRequest(userReq))
{
Thread.Sleep(500);
continue;
}
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while (State != APIState.Online)
Thread.Sleep(500);
break;
}
//hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout(false);
State = APIState.Offline;
continue;
}
//process the request queue.
APIRequest req;
while (queue.TryPeek(out req))
{
if (handleRequest(req))
{
//we have succeeded, so let's unqueue.
queue.TryDequeue(out req);
}
}
Thread.Sleep(1);
}
}
public void Login(string username, string password)
{
Debug.Assert(State == APIState.Offline);
ProvidedUsername = username;
this.password = password;
}
/// <summary>
/// Handle a single API request.
/// </summary>
/// <param name="req">The request.</param>
/// <returns>true if we should remove this request from the queue.</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;
failureCount = 0;
return true;
}
catch (WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout);
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;
}
catch (Exception e)
{
if (e is TimeoutException)
log.Add(@"API level timeout exception was hit");
req.Fail(e);
return true;
}
}
private APIState state;
public APIState State
{
get { return state; }
private set
{
APIState oldState = state;
APIState newState = value;
state = value;
if (oldState != newState)
{
log.Add($@"We just went {newState}!");
Scheduler.Add(delegate
{
components.ForEach(c => c.APIStateChanged(this, newState));
OnStateChange?.Invoke(oldState, newState);
});
}
}
}
public bool IsLoggedIn => LocalUser.Value.Id > 1;
public void Queue(APIRequest request) => queue.Enqueue(request);
public event StateChangeDelegate OnStateChange;
public delegate void StateChangeDelegate(APIState oldState, APIState newState);
private void flushQueue(bool failOldRequests = true)
{
var oldQueue = queue;
//flush the queue.
queue = new ConcurrentQueue<APIRequest>();
if (failOldRequests)
{
APIRequest req;
while (oldQueue.TryDequeue(out req))
req.Fail(new WebException(@"Disconnected from server"));
}
}
public void Logout(bool clearUsername = true)
{
flushQueue();
if (clearUsername) ProvidedUsername = null;
password = null;
authentication.Clear();
LocalUser.Value = createGuestUser();
}
private static User createGuestUser() => new User
{
Username = @"Guest",
Id = 1,
};
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
flushQueue();
cancellationToken.Cancel();
}
}
public enum APIState
{
/// <summary>
/// We cannot login (not enough credentials).
/// </summary>
Offline,
/// <summary>
/// We are having connectivity issues.
/// </summary>
Failing,
/// <summary>
/// We are in the process of (re-)connecting.
/// </summary>
Connecting,
/// <summary>
/// We are online.
/// </summary>
Online
}
}

View File

@ -1,33 +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
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public abstract class APIDownloadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Schedule(() => Progress?.Invoke(current, total));
protected APIDownloadRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
}
public event APIProgressHandler Progress;
public new event APISuccessHandler<byte[]> Success;
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public abstract class APIDownloadRequest : APIRequest
{
protected override WebRequest CreateWebRequest()
{
var request = new WebRequest(Uri);
request.DownloadProgress += request_Progress;
return request;
}
private void request_Progress(long current, long total) => API.Schedule(() => Progress?.Invoke(current, total));
protected APIDownloadRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(WebRequest.ResponseData);
}
public event APIProgressHandler Progress;
public new event APISuccessHandler<byte[]> Success;
}
}

View File

@ -1,121 +1,121 @@
// 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;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
/// <summary>
/// An API request with a well-defined response type.
/// </summary>
/// <typeparam name="T">Type of the response (used for deserialisation).</typeparam>
public abstract class APIRequest<T> : APIRequest
{
protected override WebRequest CreateWebRequest() => new JsonWebRequest<T>(Uri);
protected APIRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(((JsonWebRequest<T>)WebRequest).ResponseObject);
}
public new event APISuccessHandler<T> Success;
}
/// <summary>
/// AN API request with no specified response type.
/// </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 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;
public event APISuccessHandler Success;
public event APIFailureHandler Failure;
private bool cancelled;
private Action pendingFailure;
public void Perform(APIAccess api)
{
API = api;
if (checkAndProcessFailure())
return;
if (startTime == null)
startTime = DateTimeOffset.UtcNow;
if (remainingTime <= 0)
throw new TimeoutException(@"API request timeout hit");
WebRequest = CreateWebRequest();
WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}");
if (checkAndProcessFailure())
return;
if (!WebRequest.Aborted) //could have been aborted by a Cancel() call
WebRequest.Perform();
if (checkAndProcessFailure())
return;
api.Schedule(delegate { Success?.Invoke(); });
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
{
cancelled = true;
WebRequest?.Abort();
pendingFailure = () => Failure?.Invoke(e);
checkAndProcessFailure();
}
/// <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()
{
if (API == null || pendingFailure == null) return cancelled;
API.Schedule(pendingFailure);
pendingFailure = null;
return true;
}
}
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);
}
// 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;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
/// <summary>
/// An API request with a well-defined response type.
/// </summary>
/// <typeparam name="T">Type of the response (used for deserialisation).</typeparam>
public abstract class APIRequest<T> : APIRequest
{
protected override WebRequest CreateWebRequest() => new JsonWebRequest<T>(Uri);
protected APIRequest()
{
base.Success += onSuccess;
}
private void onSuccess()
{
Success?.Invoke(((JsonWebRequest<T>)WebRequest).ResponseObject);
}
public new event APISuccessHandler<T> Success;
}
/// <summary>
/// AN API request with no specified response type.
/// </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 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;
public event APISuccessHandler Success;
public event APIFailureHandler Failure;
private bool cancelled;
private Action pendingFailure;
public void Perform(APIAccess api)
{
API = api;
if (checkAndProcessFailure())
return;
if (startTime == null)
startTime = DateTimeOffset.UtcNow;
if (remainingTime <= 0)
throw new TimeoutException(@"API request timeout hit");
WebRequest = CreateWebRequest();
WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}");
if (checkAndProcessFailure())
return;
if (!WebRequest.Aborted) //could have been aborted by a Cancel() call
WebRequest.Perform();
if (checkAndProcessFailure())
return;
api.Schedule(delegate { Success?.Invoke(); });
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
{
cancelled = true;
WebRequest?.Abort();
pendingFailure = () => Failure?.Invoke(e);
checkAndProcessFailure();
}
/// <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()
{
if (API == null || pendingFailure == null) return cancelled;
API.Schedule(pendingFailure);
pendingFailure = null;
return true;
}
}
public delegate void APIFailureHandler(Exception e);
public delegate void APISuccessHandler();
public delegate void APIProgressHandler(long current, long total);
public delegate void APISuccessHandler<in T>(T content);
}

View File

@ -1,31 +1,31 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public class DummyAPIAccess : IAPIProvider
{
public Bindable<User> LocalUser { get; } = new Bindable<User>(new User
{
Username = @"Dummy",
Id = 1,
});
public bool IsLoggedIn => true;
public void Update()
{
}
public virtual void Queue(APIRequest request)
{
}
public void Register(IOnlineComponent component)
{
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public class DummyAPIAccess : IAPIProvider
{
public Bindable<User> LocalUser { get; } = new Bindable<User>(new User
{
Username = @"Dummy",
Id = 1,
});
public bool IsLoggedIn => true;
public void Update()
{
}
public virtual void Queue(APIRequest request)
{
}
public void Register(IOnlineComponent component)
{
}
}
}

View File

@ -1,33 +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
using osu.Framework.Configuration;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public interface IAPIProvider
{
/// <summary>
/// The local user.
/// </summary>
Bindable<User> LocalUser { get; }
/// <summary>
/// Returns whether the local user is logged in.
/// </summary>
bool IsLoggedIn { get; }
/// <summary>
/// Queue a new request.
/// </summary>
/// <param name="request">The request to perform.</param>
void Queue(APIRequest request);
/// <summary>
/// Register a component to receive state changes.
/// </summary>
/// <param name="component">The component to register.</param>
void Register(IOnlineComponent component);
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public interface IAPIProvider
{
/// <summary>
/// The local user.
/// </summary>
Bindable<User> LocalUser { get; }
/// <summary>
/// Returns whether the local user is logged in.
/// </summary>
bool IsLoggedIn { get; }
/// <summary>
/// Queue a new request.
/// </summary>
/// <param name="request">The request to perform.</param>
void Queue(APIRequest request);
/// <summary>
/// Register a component to receive state changes.
/// </summary>
/// <param name="component">The component to register.</param>
void Register(IOnlineComponent component);
}
}

View File

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

View File

@ -1,178 +1,185 @@
// 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.Diagnostics;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public class OAuth
{
private readonly string clientId;
private readonly string clientSecret;
private readonly string endpoint;
public OAuthToken Token;
internal OAuth(string clientId, string clientSecret, string endpoint)
{
Debug.Assert(clientId != null);
Debug.Assert(clientSecret != null);
Debug.Assert(endpoint != null);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.endpoint = endpoint;
}
internal bool AuthenticateWithLogin(string username, string password)
{
if (string.IsNullOrEmpty(username)) return false;
if (string.IsNullOrEmpty(password)) return false;
using (var req = new AccessTokenRequestPassword(username, password)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.POST,
ClientId = clientId,
ClientSecret = clientSecret
})
{
try
{
req.Perform();
}
catch
{
return false;
}
Token = req.ResponseObject;
return true;
}
}
internal bool AuthenticateWithRefresh(string refresh)
{
try
{
using (var req = new AccessTokenRequestRefresh(refresh)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.POST,
ClientId = clientId,
ClientSecret = clientSecret
})
{
req.Perform();
Token = req.ResponseObject;
return true;
}
}
catch
{
//todo: potentially only kill the refresh token on certain exception types.
Token = null;
return false;
}
}
private static readonly object access_token_retrieval_lock = new object();
/// <summary>
/// Should be run before any API request to make sure we have a valid key.
/// </summary>
private bool ensureAccessToken()
{
// if we already have a valid access token, let's use it.
if (accessTokenValid) return true;
// we want to ensure only a single authentication update is happening at once.
lock (access_token_retrieval_lock)
{
// re-check if valid, in case another request completed and revalidated our access.
if (accessTokenValid) return true;
// if not, let's try using our refresh token to request a new access token.
if (!string.IsNullOrEmpty(Token?.RefreshToken))
// ReSharper disable once PossibleNullReferenceException
AuthenticateWithRefresh(Token.RefreshToken);
return accessTokenValid;
}
}
private bool accessTokenValid => Token?.IsValid ?? false;
internal bool HasValidAccessToken => RequestAccessToken() != null;
internal string RequestAccessToken()
{
if (!ensureAccessToken()) return null;
return Token.AccessToken;
}
internal void Clear()
{
Token = null;
}
private class AccessTokenRequestRefresh : AccessTokenRequest
{
internal readonly string RefreshToken;
internal AccessTokenRequestRefresh(string refreshToken)
{
RefreshToken = refreshToken;
GrantType = @"refresh_token";
}
protected override void PrePerform()
{
AddParameter("refresh_token", RefreshToken);
base.PrePerform();
}
}
private class AccessTokenRequestPassword : AccessTokenRequest
{
internal readonly string Username;
internal readonly string Password;
internal AccessTokenRequestPassword(string username, string password)
{
Username = username;
Password = password;
GrantType = @"password";
}
protected override void PrePerform()
{
AddParameter("username", Username);
AddParameter("password", Password);
base.PrePerform();
}
}
private class AccessTokenRequest : JsonWebRequest<OAuthToken>
{
protected string GrantType;
internal string ClientId;
internal string ClientSecret;
protected override void PrePerform()
{
AddParameter("grant_type", GrantType);
AddParameter("client_id", ClientId);
AddParameter("client_secret", ClientSecret);
base.PrePerform();
}
}
}
}
// 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.Diagnostics;
using osu.Framework.Configuration;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API
{
public class OAuth
{
private readonly string clientId;
private readonly string clientSecret;
private readonly string endpoint;
public readonly Bindable<OAuthToken> Token = new Bindable<OAuthToken>();
public string TokenString
{
get => Token.Value?.ToString();
set => Token.Value = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value);
}
internal OAuth(string clientId, string clientSecret, string endpoint)
{
Debug.Assert(clientId != null);
Debug.Assert(clientSecret != null);
Debug.Assert(endpoint != null);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.endpoint = endpoint;
}
internal bool AuthenticateWithLogin(string username, string password)
{
if (string.IsNullOrEmpty(username)) return false;
if (string.IsNullOrEmpty(password)) return false;
using (var req = new AccessTokenRequestPassword(username, password)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.POST,
ClientId = clientId,
ClientSecret = clientSecret
})
{
try
{
req.Perform();
}
catch
{
return false;
}
Token.Value = req.ResponseObject;
return true;
}
}
internal bool AuthenticateWithRefresh(string refresh)
{
try
{
using (var req = new AccessTokenRequestRefresh(refresh)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.POST,
ClientId = clientId,
ClientSecret = clientSecret
})
{
req.Perform();
Token.Value = req.ResponseObject;
return true;
}
}
catch
{
//todo: potentially only kill the refresh token on certain exception types.
Token.Value = null;
return false;
}
}
private static readonly object access_token_retrieval_lock = new object();
/// <summary>
/// Should be run before any API request to make sure we have a valid key.
/// </summary>
private bool ensureAccessToken()
{
// if we already have a valid access token, let's use it.
if (accessTokenValid) return true;
// we want to ensure only a single authentication update is happening at once.
lock (access_token_retrieval_lock)
{
// re-check if valid, in case another request completed and revalidated our access.
if (accessTokenValid) return true;
// if not, let's try using our refresh token to request a new access token.
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
// ReSharper disable once PossibleNullReferenceException
AuthenticateWithRefresh(Token.Value.RefreshToken);
return accessTokenValid;
}
}
private bool accessTokenValid => Token.Value?.IsValid ?? false;
internal bool HasValidAccessToken => RequestAccessToken() != null;
internal string RequestAccessToken()
{
if (!ensureAccessToken()) return null;
return Token.Value.AccessToken;
}
internal void Clear()
{
Token.Value = null;
}
private class AccessTokenRequestRefresh : AccessTokenRequest
{
internal readonly string RefreshToken;
internal AccessTokenRequestRefresh(string refreshToken)
{
RefreshToken = refreshToken;
GrantType = @"refresh_token";
}
protected override void PrePerform()
{
AddParameter("refresh_token", RefreshToken);
base.PrePerform();
}
}
private class AccessTokenRequestPassword : AccessTokenRequest
{
internal readonly string Username;
internal readonly string Password;
internal AccessTokenRequestPassword(string username, string password)
{
Username = username;
Password = password;
GrantType = @"password";
}
protected override void PrePerform()
{
AddParameter("username", Username);
AddParameter("password", Password);
base.PrePerform();
}
}
private class AccessTokenRequest : JsonWebRequest<OAuthToken>
{
protected string GrantType;
internal string ClientId;
internal string ClientSecret;
protected override void PrePerform()
{
AddParameter("grant_type", GrantType);
AddParameter("client_id", ClientId);
AddParameter("client_secret", ClientSecret);
base.PrePerform();
}
}
}
}

View File

@ -1,63 +1,63 @@
// 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;
using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Online.API
{
[Serializable]
public class OAuthToken
{
/// <summary>
/// OAuth 2.0 access token.
/// </summary>
[JsonProperty(@"access_token")]
public string AccessToken;
[JsonProperty(@"expires_in")]
public long ExpiresIn
{
get
{
return AccessTokenExpiry - DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
set
{
AccessTokenExpiry = DateTimeOffset.Now.AddSeconds(value).ToUnixTimeSeconds();
}
}
public bool IsValid => !string.IsNullOrEmpty(AccessToken) && ExpiresIn > 30;
public long AccessTokenExpiry;
/// <summary>
/// OAuth 2.0 refresh token.
/// </summary>
[JsonProperty(@"refresh_token")]
public string RefreshToken;
public override string ToString() => $@"{AccessToken}|{AccessTokenExpiry.ToString(NumberFormatInfo.InvariantInfo)}|{RefreshToken}";
public static OAuthToken Parse(string value)
{
try
{
string[] parts = value.Split('|');
return new OAuthToken
{
AccessToken = parts[0],
AccessTokenExpiry = long.Parse(parts[1], NumberFormatInfo.InvariantInfo),
RefreshToken = parts[2]
};
}
catch
{
}
return null;
}
}
}
// 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;
using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Online.API
{
[Serializable]
public class OAuthToken
{
/// <summary>
/// OAuth 2.0 access token.
/// </summary>
[JsonProperty(@"access_token")]
public string AccessToken;
[JsonProperty(@"expires_in")]
public long ExpiresIn
{
get
{
return AccessTokenExpiry - DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
set
{
AccessTokenExpiry = DateTimeOffset.Now.AddSeconds(value).ToUnixTimeSeconds();
}
}
public bool IsValid => !string.IsNullOrEmpty(AccessToken) && ExpiresIn > 30;
public long AccessTokenExpiry;
/// <summary>
/// OAuth 2.0 refresh token.
/// </summary>
[JsonProperty(@"refresh_token")]
public string RefreshToken;
public override string ToString() => $@"{AccessToken}|{AccessTokenExpiry.ToString(NumberFormatInfo.InvariantInfo)}|{RefreshToken}";
public static OAuthToken Parse(string value)
{
try
{
string[] parts = value.Split('|');
return new OAuthToken
{
AccessToken = parts[0],
AccessTokenExpiry = long.Parse(parts[1], NumberFormatInfo.InvariantInfo),
RefreshToken = parts[2]
};
}
catch
{
}
return null;
}
}
}

View File

@ -1,145 +1,145 @@
// 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 System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using System;
namespace osu.Game.Online.API.Requests
{
public class APIResponseBeatmapSet : BeatmapMetadata // todo: this is a bit wrong...
{
[JsonProperty(@"covers")]
private BeatmapSetOnlineCovers covers { get; set; }
[JsonProperty(@"preview_url")]
private string preview { get; set; }
[JsonProperty(@"play_count")]
private int playCount { get; set; }
[JsonProperty(@"favourite_count")]
private int favouriteCount { get; set; }
[JsonProperty(@"bpm")]
private double bpm { get; set; }
[JsonProperty(@"video")]
private bool hasVideo { get; set; }
[JsonProperty(@"status")]
private BeatmapSetOnlineStatus status { get; set; }
[JsonProperty(@"submitted_date")]
private DateTimeOffset submitted { get; set; }
[JsonProperty(@"ranked_date")]
private DateTimeOffset ranked { get; set; }
[JsonProperty(@"last_updated")]
private DateTimeOffset lastUpdated { get; set; }
[JsonProperty(@"user_id")]
private long creatorId {
set { Author.Id = value; }
}
[JsonProperty(@"beatmaps")]
private IEnumerable<APIResponseBeatmap> beatmaps { get; set; }
public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
{
return new BeatmapSetInfo
{
OnlineBeatmapSetID = OnlineBeatmapSetID,
Metadata = this,
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = covers,
Preview = preview,
PlayCount = playCount,
FavouriteCount = favouriteCount,
BPM = bpm,
Status = status,
HasVideo = hasVideo,
Submitted = submitted,
Ranked = ranked,
LastUpdated = lastUpdated,
},
Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(),
};
}
private class APIResponseBeatmap : BeatmapMetadata
{
[JsonProperty(@"id")]
private int onlineBeatmapID { get; set; }
[JsonProperty(@"playcount")]
private int playCount { get; set; }
[JsonProperty(@"passcount")]
private int passCount { get; set; }
[JsonProperty(@"mode_int")]
private int ruleset { get; set; }
[JsonProperty(@"difficulty_rating")]
private double starDifficulty { get; set; }
[JsonProperty(@"drain")]
private float drainRate { get; set; }
[JsonProperty(@"cs")]
private float circleSize { get; set; }
[JsonProperty(@"ar")]
private float approachRate { get; set; }
[JsonProperty(@"accuracy")]
private float overallDifficulty { get; set; }
[JsonProperty(@"total_length")]
private double length { get; set; }
[JsonProperty(@"count_circles")]
private int circleCount { get; set; }
[JsonProperty(@"count_sliders")]
private int sliderCount { get; set; }
[JsonProperty(@"version")]
private string version { get; set; }
public BeatmapInfo ToBeatmap(RulesetStore rulesets)
{
return new BeatmapInfo
{
Metadata = this,
Ruleset = rulesets.GetRuleset(ruleset),
StarDifficulty = starDifficulty,
OnlineBeatmapID = onlineBeatmapID,
Version = version,
BaseDifficulty = new BeatmapDifficulty
{
DrainRate = drainRate,
CircleSize = circleSize,
ApproachRate = approachRate,
OverallDifficulty = overallDifficulty,
},
OnlineInfo = new BeatmapOnlineInfo
{
PlayCount = playCount,
PassCount = passCount,
Length = length,
CircleCount = circleCount,
SliderCount = sliderCount,
},
};
}
}
}
}
// 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 System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using System;
namespace osu.Game.Online.API.Requests
{
public class APIResponseBeatmapSet : BeatmapMetadata // todo: this is a bit wrong...
{
[JsonProperty(@"covers")]
private BeatmapSetOnlineCovers covers { get; set; }
[JsonProperty(@"preview_url")]
private string preview { get; set; }
[JsonProperty(@"play_count")]
private int playCount { get; set; }
[JsonProperty(@"favourite_count")]
private int favouriteCount { get; set; }
[JsonProperty(@"bpm")]
private double bpm { get; set; }
[JsonProperty(@"video")]
private bool hasVideo { get; set; }
[JsonProperty(@"status")]
private BeatmapSetOnlineStatus status { get; set; }
[JsonProperty(@"submitted_date")]
private DateTimeOffset submitted { get; set; }
[JsonProperty(@"ranked_date")]
private DateTimeOffset ranked { get; set; }
[JsonProperty(@"last_updated")]
private DateTimeOffset lastUpdated { get; set; }
[JsonProperty(@"user_id")]
private long creatorId {
set { Author.Id = value; }
}
[JsonProperty(@"beatmaps")]
private IEnumerable<APIResponseBeatmap> beatmaps { get; set; }
public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
{
return new BeatmapSetInfo
{
OnlineBeatmapSetID = OnlineBeatmapSetID,
Metadata = this,
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = covers,
Preview = preview,
PlayCount = playCount,
FavouriteCount = favouriteCount,
BPM = bpm,
Status = status,
HasVideo = hasVideo,
Submitted = submitted,
Ranked = ranked,
LastUpdated = lastUpdated,
},
Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(),
};
}
private class APIResponseBeatmap : BeatmapMetadata
{
[JsonProperty(@"id")]
private int onlineBeatmapID { get; set; }
[JsonProperty(@"playcount")]
private int playCount { get; set; }
[JsonProperty(@"passcount")]
private int passCount { get; set; }
[JsonProperty(@"mode_int")]
private int ruleset { get; set; }
[JsonProperty(@"difficulty_rating")]
private double starDifficulty { get; set; }
[JsonProperty(@"drain")]
private float drainRate { get; set; }
[JsonProperty(@"cs")]
private float circleSize { get; set; }
[JsonProperty(@"ar")]
private float approachRate { get; set; }
[JsonProperty(@"accuracy")]
private float overallDifficulty { get; set; }
[JsonProperty(@"total_length")]
private double length { get; set; }
[JsonProperty(@"count_circles")]
private int circleCount { get; set; }
[JsonProperty(@"count_sliders")]
private int sliderCount { get; set; }
[JsonProperty(@"version")]
private string version { get; set; }
public BeatmapInfo ToBeatmap(RulesetStore rulesets)
{
return new BeatmapInfo
{
Metadata = this,
Ruleset = rulesets.GetRuleset(ruleset),
StarDifficulty = starDifficulty,
OnlineBeatmapID = onlineBeatmapID,
Version = version,
BaseDifficulty = new BeatmapDifficulty
{
DrainRate = drainRate,
CircleSize = circleSize,
ApproachRate = approachRate,
OverallDifficulty = overallDifficulty,
},
OnlineInfo = new BeatmapOnlineInfo
{
PlayCount = playCount,
PassCount = passCount,
Length = length,
CircleCount = circleCount,
SliderCount = sliderCount,
},
};
}
}
}
}

View File

@ -1,27 +1,27 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using System;
namespace osu.Game.Online.API.Requests
{
public class DownloadBeatmapSetRequest : APIDownloadRequest
{
public readonly BeatmapSetInfo BeatmapSet;
public Action<float> DownloadProgressed;
private readonly bool noVideo;
public DownloadBeatmapSetRequest(BeatmapSetInfo set, bool noVideo)
{
this.noVideo = noVideo;
BeatmapSet = set;
Progress += (current, total) => DownloadProgressed?.Invoke((float) current / total);
}
protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using System;
namespace osu.Game.Online.API.Requests
{
public class DownloadBeatmapSetRequest : APIDownloadRequest
{
public readonly BeatmapSetInfo BeatmapSet;
public Action<float> DownloadProgressed;
private readonly bool noVideo;
public DownloadBeatmapSetRequest(BeatmapSetInfo set, bool noVideo)
{
this.noVideo = noVideo;
BeatmapSet = set;
Progress += (current, total) => DownloadProgressed?.Invoke((float) current / total);
}
protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
}
}

View File

@ -1,46 +1,46 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Beatmaps;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapDetailsRequest : APIRequest<GetBeatmapDetailsResponse>
{
private readonly BeatmapInfo beatmap;
private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}";
public GetBeatmapDetailsRequest(BeatmapInfo beatmap)
{
this.beatmap = beatmap;
}
protected override string Target => $@"beatmaps/{lookupString}";
}
public class GetBeatmapDetailsResponse : BeatmapMetrics
{
//the online API returns some metrics as a nested object.
[JsonProperty(@"failtimes")]
private BeatmapMetrics failTimes
{
set
{
Fails = value.Fails;
Retries = value.Retries;
}
}
//and other metrics in the beatmap set.
[JsonProperty(@"beatmapset")]
private BeatmapMetrics beatmapSet
{
set
{
Ratings = value.Ratings;
}
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Beatmaps;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapDetailsRequest : APIRequest<GetBeatmapDetailsResponse>
{
private readonly BeatmapInfo beatmap;
private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}";
public GetBeatmapDetailsRequest(BeatmapInfo beatmap)
{
this.beatmap = beatmap;
}
protected override string Target => $@"beatmaps/{lookupString}";
}
public class GetBeatmapDetailsResponse : BeatmapMetrics
{
//the online API returns some metrics as a nested object.
[JsonProperty(@"failtimes")]
private BeatmapMetrics failTimes
{
set
{
Fails = value.Fails;
Retries = value.Retries;
}
}
//and other metrics in the beatmap set.
[JsonProperty(@"beatmapset")]
private BeatmapMetrics beatmapSet
{
set
{
Ratings = value.Ratings;
}
}
}
}

View File

@ -1,17 +1,17 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapSetRequest : APIRequest<APIResponseBeatmapSet>
{
private readonly int beatmapSetId;
public GetBeatmapSetRequest(int beatmapSetId)
{
this.beatmapSetId = beatmapSetId;
}
protected override string Target => $@"beatmapsets/{beatmapSetId}";
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapSetRequest : APIRequest<APIResponseBeatmapSet>
{
private readonly int beatmapSetId;
public GetBeatmapSetRequest(int beatmapSetId)
{
this.beatmapSetId = beatmapSetId;
}
protected override string Target => $@"beatmapsets/{beatmapSetId}";
}
}

View File

@ -1,13 +1,13 @@
// 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 osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetFriendsRequest : APIRequest<List<User>>
{
protected override string Target => @"friends";
}
}
// 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 osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetFriendsRequest : APIRequest<List<User>>
{
protected override string Target => @"friends";
}
}

View File

@ -1,38 +1,38 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class GetMessagesRequest : APIMessagesRequest
{
private readonly IEnumerable<Channel> channels;
public GetMessagesRequest(IEnumerable<Channel> channels, long? sinceId) : base(sinceId)
{
if (channels == null)
throw new ArgumentNullException(nameof(channels));
if (channels.Any(c => c.Target != TargetType.Channel))
throw new ArgumentException("All channels in the argument channels must have the targettype Channel");
this.channels = channels;
}
protected override WebRequest CreateWebRequest()
{
string channelString = string.Join(",", channels.Select(x => x.Id));
var req = base.CreateWebRequest();
req.AddParameter(@"channels", channelString);
return req;
}
protected override string Target => @"chat/messages";
}
}
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class GetMessagesRequest : APIMessagesRequest
{
private readonly IEnumerable<Channel> channels;
public GetMessagesRequest(IEnumerable<Channel> channels, long? sinceId) : base(sinceId)
{
if (channels == null)
throw new ArgumentNullException(nameof(channels));
if (channels.Any(c => c.Target != TargetType.Channel))
throw new ArgumentException("All channels in the argument channels must have the targettype Channel");
this.channels = channels;
}
protected override WebRequest CreateWebRequest()
{
string channelString = string.Join(",", channels.Select(x => x.Id));
var req = base.CreateWebRequest();
req.AddParameter(@"channels", channelString);
return req;
}
protected override string Target => @"chat/messages";
}
}

View File

@ -1,166 +1,166 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Users;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<GetScoresResponse>
{
private readonly BeatmapInfo beatmap;
private readonly LeaderboardScope scope;
private readonly RulesetInfo ruleset;
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, LeaderboardScope scope = LeaderboardScope.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)
throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard");
this.beatmap = beatmap;
this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
Success += onSuccess;
}
private void onSuccess(GetScoresResponse r)
{
foreach (OnlineScore score in r.Scores)
score.ApplyBeatmap(beatmap);
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Timeout = 30000;
req.AddParameter(@"type", scope.ToString().ToLowerInvariant());
req.AddParameter(@"mode", ruleset.ShortName);
return req;
}
protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores";
}
public class GetScoresResponse
{
[JsonProperty(@"scores")]
public IEnumerable<OnlineScore> Scores;
}
public class OnlineScore : Score
{
[JsonProperty(@"score")]
private double totalScore
{
set { TotalScore = value; }
}
[JsonProperty(@"max_combo")]
private int maxCombo
{
set { MaxCombo = value; }
}
[JsonProperty(@"user")]
private User user
{
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
{
set { OnlineScoreID = value; }
}
[JsonProperty(@"created_at")]
private DateTimeOffset date
{
set { Date = value; }
}
[JsonProperty(@"beatmap")]
private BeatmapInfo beatmap
{
set { Beatmap = value; }
}
[JsonProperty(@"beatmapset")]
private BeatmapMetadata metadata
{
set { Beatmap.Metadata = value; }
}
[JsonProperty(@"statistics")]
private Dictionary<string, object> jsonStats
{
set
{
foreach (var kvp in value)
{
HitResult newKey;
switch (kvp.Key)
{
case @"count_300":
newKey = HitResult.Great;
break;
case @"count_100":
newKey = HitResult.Good;
break;
case @"count_50":
newKey = HitResult.Meh;
break;
case @"count_miss":
newKey = HitResult.Miss;
break;
default:
continue;
}
Statistics.Add(newKey, kvp.Value);
}
}
}
[JsonProperty(@"mods")]
private string[] modStrings { get; set; }
public void ApplyBeatmap(BeatmapInfo beatmap)
{
Beatmap = beatmap;
ApplyRuleset(beatmap.Ruleset);
}
public void ApplyRuleset(RulesetInfo ruleset)
{
Ruleset = ruleset;
// Evaluate the mod string
Mods = Ruleset.CreateInstance().GetAllMods().Where(mod => modStrings.Contains(mod.ShortenedName)).ToArray();
}
}
}
// 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;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Users;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<GetScoresResponse>
{
private readonly BeatmapInfo beatmap;
private readonly LeaderboardScope scope;
private readonly RulesetInfo ruleset;
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, LeaderboardScope scope = LeaderboardScope.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)
throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard");
this.beatmap = beatmap;
this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
Success += onSuccess;
}
private void onSuccess(GetScoresResponse r)
{
foreach (OnlineScore score in r.Scores)
score.ApplyBeatmap(beatmap);
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Timeout = 30000;
req.AddParameter(@"type", scope.ToString().ToLowerInvariant());
req.AddParameter(@"mode", ruleset.ShortName);
return req;
}
protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores";
}
public class GetScoresResponse
{
[JsonProperty(@"scores")]
public IEnumerable<OnlineScore> Scores;
}
public class OnlineScore : Score
{
[JsonProperty(@"score")]
private double totalScore
{
set { TotalScore = value; }
}
[JsonProperty(@"max_combo")]
private int maxCombo
{
set { MaxCombo = value; }
}
[JsonProperty(@"user")]
private User user
{
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
{
set { OnlineScoreID = value; }
}
[JsonProperty(@"created_at")]
private DateTimeOffset date
{
set { Date = value; }
}
[JsonProperty(@"beatmap")]
private BeatmapInfo beatmap
{
set { Beatmap = value; }
}
[JsonProperty(@"beatmapset")]
private BeatmapMetadata metadata
{
set { Beatmap.Metadata = value; }
}
[JsonProperty(@"statistics")]
private Dictionary<string, object> jsonStats
{
set
{
foreach (var kvp in value)
{
HitResult newKey;
switch (kvp.Key)
{
case @"count_300":
newKey = HitResult.Great;
break;
case @"count_100":
newKey = HitResult.Good;
break;
case @"count_50":
newKey = HitResult.Meh;
break;
case @"count_miss":
newKey = HitResult.Miss;
break;
default:
continue;
}
Statistics.Add(newKey, kvp.Value);
}
}
}
[JsonProperty(@"mods")]
private string[] modStrings { get; set; }
public void ApplyBeatmap(BeatmapInfo beatmap)
{
Beatmap = beatmap;
ApplyRuleset(beatmap.Ruleset);
}
public void ApplyRuleset(RulesetInfo ruleset)
{
Ruleset = ruleset;
// Evaluate the mod string
Mods = Ruleset.CreateInstance().GetAllMods().Where(mod => modStrings.Contains(mod.ShortenedName)).ToArray();
}
}
}

View File

@ -1,33 +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
using Humanizer;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserBeatmapsRequest : APIRequest<List<APIResponseBeatmapSet>>
{
private readonly long userId;
private readonly int offset;
private readonly BeatmapSetType type;
public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int offset = 0)
{
this.userId = userId;
this.offset = offset;
this.type = type;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}?offset={offset}";
}
public enum BeatmapSetType
{
Favourite,
RankedAndApproved,
Unranked,
Graveyard
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Humanizer;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserBeatmapsRequest : APIRequest<List<APIResponseBeatmapSet>>
{
private readonly long userId;
private readonly int offset;
private readonly BeatmapSetType type;
public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int offset = 0)
{
this.userId = userId;
this.offset = offset;
this.type = type;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}?offset={offset}";
}
public enum BeatmapSetType
{
Favourite,
RankedAndApproved,
Unranked,
Graveyard
}
}

View File

@ -1,48 +1,48 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserMostPlayedBeatmapsRequest : APIRequest<List<MostPlayedBeatmap>>
{
private readonly long userId;
private readonly int offset;
public GetUserMostPlayedBeatmapsRequest(long userId, int offset = 0)
{
this.userId = userId;
this.offset = offset;
}
protected override string Target => $@"users/{userId}/beatmapsets/most_played?offset={offset}";
}
public class MostPlayedBeatmap
{
[JsonProperty("beatmap_id")]
public int BeatmapID;
[JsonProperty("count")]
public int PlayCount;
[JsonProperty]
private BeatmapInfo beatmap;
[JsonProperty]
private APIResponseBeatmapSet beatmapSet;
public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets)
{
BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets);
beatmap.BeatmapSet = setInfo;
beatmap.OnlineBeatmapSetID = setInfo.OnlineBeatmapSetID;
beatmap.Metadata = setInfo.Metadata;
return beatmap;
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserMostPlayedBeatmapsRequest : APIRequest<List<MostPlayedBeatmap>>
{
private readonly long userId;
private readonly int offset;
public GetUserMostPlayedBeatmapsRequest(long userId, int offset = 0)
{
this.userId = userId;
this.offset = offset;
}
protected override string Target => $@"users/{userId}/beatmapsets/most_played?offset={offset}";
}
public class MostPlayedBeatmap
{
[JsonProperty("beatmap_id")]
public int BeatmapID;
[JsonProperty("count")]
public int PlayCount;
[JsonProperty]
private BeatmapInfo beatmap;
[JsonProperty]
private APIResponseBeatmapSet beatmapSet;
public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets)
{
BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets);
beatmap.BeatmapSet = setInfo;
beatmap.OnlineBeatmapSetID = setInfo.OnlineBeatmapSetID;
beatmap.Metadata = setInfo.Metadata;
return beatmap;
}
}
}

View File

@ -1,130 +1,130 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Rulesets.Scoring;
using Humanizer;
using System;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserRecentActivitiesRequest : APIRequest<List<RecentActivity>>
{
private readonly long userId;
private readonly int offset;
public GetUserRecentActivitiesRequest(long userId, int offset = 0)
{
this.userId = userId;
this.offset = offset;
}
protected override string Target => $"users/{userId}/recent_activity?offset={offset}";
}
public class RecentActivity
{
[JsonProperty("id")]
public int ID;
[JsonProperty("createdAt")]
public DateTimeOffset CreatedAt;
[JsonProperty]
private string type
{
set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize());
}
public RecentActivityType Type;
[JsonProperty]
private string scoreRank
{
set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
}
public ScoreRank ScoreRank;
[JsonProperty("rank")]
public int Rank;
[JsonProperty("approval")]
public BeatmapApproval Approval;
[JsonProperty("count")]
public int Count;
[JsonProperty("mode")]
public string Mode;
[JsonProperty("beatmap")]
public RecentActivityBeatmap Beatmap;
[JsonProperty("beatmapset")]
public RecentActivityBeatmap Beatmapset;
[JsonProperty("user")]
public RecentActivityUser User;
[JsonProperty("achievement")]
public RecentActivityAchievement Achievement;
public class RecentActivityBeatmap
{
[JsonProperty("title")]
public string Title;
[JsonProperty("url")]
public string Url;
}
public class RecentActivityUser
{
[JsonProperty("username")]
public string Username;
[JsonProperty("url")]
public string Url;
[JsonProperty("previousUsername")]
public string PreviousUsername;
}
public class RecentActivityAchievement
{
[JsonProperty("slug")]
public string Slug;
[JsonProperty("name")]
public string Name;
}
}
public enum RecentActivityType
{
Achievement,
BeatmapPlaycount,
BeatmapsetApprove,
BeatmapsetDelete,
BeatmapsetRevive,
BeatmapsetUpdate,
BeatmapsetUpload,
Medal,
Rank,
RankLost,
UserSupportAgain,
UserSupportFirst,
UserSupportGift,
UsernameChange,
}
public enum BeatmapApproval
{
Ranked,
Approved,
Qualified,
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Rulesets.Scoring;
using Humanizer;
using System;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests
{
public class GetUserRecentActivitiesRequest : APIRequest<List<RecentActivity>>
{
private readonly long userId;
private readonly int offset;
public GetUserRecentActivitiesRequest(long userId, int offset = 0)
{
this.userId = userId;
this.offset = offset;
}
protected override string Target => $"users/{userId}/recent_activity?offset={offset}";
}
public class RecentActivity
{
[JsonProperty("id")]
public int ID;
[JsonProperty("createdAt")]
public DateTimeOffset CreatedAt;
[JsonProperty]
private string type
{
set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize());
}
public RecentActivityType Type;
[JsonProperty]
private string scoreRank
{
set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
}
public ScoreRank ScoreRank;
[JsonProperty("rank")]
public int Rank;
[JsonProperty("approval")]
public BeatmapApproval Approval;
[JsonProperty("count")]
public int Count;
[JsonProperty("mode")]
public string Mode;
[JsonProperty("beatmap")]
public RecentActivityBeatmap Beatmap;
[JsonProperty("beatmapset")]
public RecentActivityBeatmap Beatmapset;
[JsonProperty("user")]
public RecentActivityUser User;
[JsonProperty("achievement")]
public RecentActivityAchievement Achievement;
public class RecentActivityBeatmap
{
[JsonProperty("title")]
public string Title;
[JsonProperty("url")]
public string Url;
}
public class RecentActivityUser
{
[JsonProperty("username")]
public string Username;
[JsonProperty("url")]
public string Url;
[JsonProperty("previousUsername")]
public string PreviousUsername;
}
public class RecentActivityAchievement
{
[JsonProperty("slug")]
public string Slug;
[JsonProperty("name")]
public string Name;
}
}
public enum RecentActivityType
{
Achievement,
BeatmapPlaycount,
BeatmapsetApprove,
BeatmapsetDelete,
BeatmapsetRevive,
BeatmapsetUpdate,
BeatmapsetUpload,
Medal,
Rank,
RankLost,
UserSupportAgain,
UserSupportFirst,
UserSupportGift,
UsernameChange,
}
public enum BeatmapApproval
{
Ranked,
Approved,
Qualified,
}
}

View File

@ -1,19 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest<User>
{
private long? userId;
public GetUserRequest(long? userId = null)
{
this.userId = userId;
}
protected override string Target => userId.HasValue ? $@"users/{userId}" : @"me";
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest<User>
{
private long? userId;
public GetUserRequest(long? userId = null)
{
this.userId = userId;
}
protected override string Target => userId.HasValue ? $@"users/{userId}" : @"me";
}
}

View File

@ -1,31 +1,31 @@
// 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;
namespace osu.Game.Online.API.Requests
{
public class GetUserScoresRequest : APIRequest<List<OnlineScore>>
{
private readonly long userId;
private readonly ScoreType type;
private readonly int offset;
public GetUserScoresRequest(long userId, ScoreType type, int offset = 0)
{
this.userId = userId;
this.type = type;
this.offset = offset;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLower()}?offset={offset}";
}
public enum ScoreType
{
Best,
Firsts,
Recent
}
}
// 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;
namespace osu.Game.Online.API.Requests
{
public class GetUserScoresRequest : APIRequest<List<OnlineScore>>
{
private readonly long userId;
private readonly ScoreType type;
private readonly int offset;
public GetUserScoresRequest(long userId, ScoreType type, int offset = 0)
{
this.userId = userId;
this.type = type;
this.offset = offset;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLower()}?offset={offset}";
}
public enum ScoreType
{
Best,
Firsts,
Recent
}
}

View File

@ -1,20 +1,20 @@
// 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;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest<List<RankingEntry>>
{
protected override string Target => @"rankings/osu/performance";
}
public class RankingEntry
{
[JsonProperty]
public User User;
}
}
// 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;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest<List<RankingEntry>>
{
protected override string Target => @"rankings/osu/performance";
}
public class RankingEntry
{
[JsonProperty]
public User User;
}
}

View File

@ -1,13 +1,13 @@
// 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 osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class ListChannelsRequest : APIRequest<List<Channel>>
{
protected override string Target => @"chat/channels";
}
}
// 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 osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class ListChannelsRequest : APIRequest<List<Channel>>
{
protected override string Target => @"chat/channels";
}
}

View File

@ -1,34 +1,34 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Extensions;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class PostMessageRequest : APIRequest<Message>
{
private readonly Message message;
public PostMessageRequest(Message message)
{
this.message = message;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.POST;
req.AddParameter(@"target_type", message.TargetType.GetDescription());
req.AddParameter(@"target_id", message.TargetId.ToString());
req.AddParameter(@"is_action", message.IsAction.ToString().ToLower());
req.AddParameter(@"message", message.Content);
return req;
}
protected override string Target => @"chat/messages";
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Extensions;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
namespace osu.Game.Online.API.Requests
{
public class PostMessageRequest : APIRequest<Message>
{
private readonly Message message;
public PostMessageRequest(Message message)
{
this.message = message;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.POST;
req.AddParameter(@"target_type", message.TargetType.GetDescription());
req.AddParameter(@"target_id", message.TargetId.ToString());
req.AddParameter(@"is_action", message.IsAction.ToString().ToLower());
req.AddParameter(@"message", message.Content);
return req;
}
protected override string Target => @"chat/messages";
}
}

View File

@ -1,33 +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
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest<IEnumerable<APIResponseBeatmapSet>>
{
private readonly string query;
private readonly RulesetInfo ruleset;
private readonly RankStatus rankStatus;
private readonly DirectSortCriteria sortCriteria;
private readonly SortDirection direction;
private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, RankStatus rankStatus = RankStatus.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending)
{
this.query = System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
this.rankStatus = rankStatus;
this.sortCriteria = sortCriteria;
this.direction = direction;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={(int)rankStatus}&sort={sortCriteria.ToString().ToLower()}_{directionString}";
}
}
// 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 osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest<IEnumerable<APIResponseBeatmapSet>>
{
private readonly string query;
private readonly RulesetInfo ruleset;
private readonly RankStatus rankStatus;
private readonly DirectSortCriteria sortCriteria;
private readonly SortDirection direction;
private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, RankStatus rankStatus = RankStatus.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending)
{
this.query = System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
this.rankStatus = rankStatus;
this.sortCriteria = sortCriteria;
this.direction = direction;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={(int)rankStatus}&sort={sortCriteria.ToString().ToLower()}_{directionString}";
}
}

View File

@ -1,123 +1,123 @@
// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Configuration;
using osu.Framework.Lists;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class Channel
{
public const int MAX_HISTORY = 300;
[JsonProperty(@"name")]
public string Name;
[JsonProperty(@"description")]
public string Topic;
[JsonProperty(@"type")]
public string Type;
[JsonProperty(@"channel_id")]
public long Id;
[JsonConstructor]
public Channel()
{
}
/// <summary>
/// Contructs a private channel
/// TODO this class needs to be serialized from something like channels/private, instead of creating from a contructor
/// </summary>
/// <param name="user">The user </param>
public Channel(User user)
{
Target = TargetType.User;
Name = user.Username;
Id = user.Id;
JoinedUsers.Add(user);
}
/// <summary>
/// Contains every joined user except yourself
/// </summary>
public ObservableCollection<User> JoinedUsers = new ObservableCollection<User>();
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
public event Action<IEnumerable<Message>> NewMessagesArrived;
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
public event Action<Message> MessageRemoved;
public Bindable<bool> Joined = new Bindable<bool>();
public TargetType Target { get; set; }
public bool ReadOnly { get; set; }
public void AddLocalEcho(LocalEchoMessage message)
{
pendingMessages.Add(message);
Messages.Add(message);
NewMessagesArrived?.Invoke(new[] { message });
}
public void AddNewMessages(params Message[] messages)
{
messages = messages.Except(Messages).ToArray();
Messages.AddRange(messages);
purgeOldMessages();
NewMessagesArrived?.Invoke(messages);
}
/// <summary>
/// Replace or remove a message from the channel.
/// </summary>
/// <param name="echo">The local echo message (client-side).</param>
/// <param name="final">The response message, or null if the message became invalid.</param>
public void ReplaceMessage(LocalEchoMessage echo, Message final)
{
if (!pendingMessages.Remove(echo))
throw new InvalidOperationException("Attempted to remove echo that wasn't present");
Messages.Remove(echo);
if (final == null)
{
MessageRemoved?.Invoke(echo);
return;
}
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;
}
Messages.Add(final);
PendingMessageResolved?.Invoke(echo, final);
}
private void purgeOldMessages()
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MAX_HISTORY)
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
}
public override string ToString() => Name;
}
}
// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Configuration;
using osu.Framework.Lists;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class Channel
{
public const int MAX_HISTORY = 300;
[JsonProperty(@"name")]
public string Name;
[JsonProperty(@"description")]
public string Topic;
[JsonProperty(@"type")]
public string Type;
[JsonProperty(@"channel_id")]
public long Id;
[JsonConstructor]
public Channel()
{
}
/// <summary>
/// Contructs a private channel
/// TODO this class needs to be serialized from something like channels/private, instead of creating from a contructor
/// </summary>
/// <param name="user">The user </param>
public Channel(User user)
{
Target = TargetType.User;
Name = user.Username;
Id = user.Id;
JoinedUsers.Add(user);
}
/// <summary>
/// Contains every joined user except yourself
/// </summary>
public ObservableCollection<User> JoinedUsers = new ObservableCollection<User>();
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
public event Action<IEnumerable<Message>> NewMessagesArrived;
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
public event Action<Message> MessageRemoved;
public Bindable<bool> Joined = new Bindable<bool>();
public TargetType Target { get; set; }
public bool ReadOnly { get; set; }
public void AddLocalEcho(LocalEchoMessage message)
{
pendingMessages.Add(message);
Messages.Add(message);
NewMessagesArrived?.Invoke(new[] { message });
}
public void AddNewMessages(params Message[] messages)
{
messages = messages.Except(Messages).ToArray();
Messages.AddRange(messages);
purgeOldMessages();
NewMessagesArrived?.Invoke(messages);
}
/// <summary>
/// Replace or remove a message from the channel.
/// </summary>
/// <param name="echo">The local echo message (client-side).</param>
/// <param name="final">The response message, or null if the message became invalid.</param>
public void ReplaceMessage(LocalEchoMessage echo, Message final)
{
if (!pendingMessages.Remove(echo))
throw new InvalidOperationException("Attempted to remove echo that wasn't present");
Messages.Remove(echo);
if (final == null)
{
MessageRemoved?.Invoke(echo);
return;
}
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;
}
Messages.Add(final);
PendingMessageResolved?.Invoke(echo, final);
}
private void purgeOldMessages()
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MAX_HISTORY)
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
}
public override string ToString() => Name;
}
}

View File

@ -1,59 +1,59 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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;
namespace osu.Game.Online.Chat
{
/// <summary>
/// An invisible drawable that brings multiple <see cref="SpriteText"/> 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 override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(IEnumerable<SpriteText> parts)
{
Parts = parts.ToList();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IdleColour = colours.Blue;
}
protected override IEnumerable<Drawable> EffectTargets => Parts;
public string TooltipText { get; set; }
private class LinkHoverSounds : HoverClickSounds
{
private readonly List<SpriteText> parts;
public LinkHoverSounds(HoverSampleSet sampleSet, List<SpriteText> parts)
: base(sampleSet)
{
this.parts = parts;
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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;
namespace osu.Game.Online.Chat
{
/// <summary>
/// An invisible drawable that brings multiple <see cref="SpriteText"/> 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 override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(IEnumerable<SpriteText> parts)
{
Parts = parts.ToList();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IdleColour = colours.Blue;
}
protected override IEnumerable<Drawable> EffectTargets => Parts;
public string TooltipText { get; set; }
private class LinkHoverSounds : HoverClickSounds
{
private readonly List<SpriteText> parts;
public LinkHoverSounds(HoverSampleSet sampleSet, List<SpriteText> parts)
: base(sampleSet)
{
this.parts = parts;
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
}
}
}

View File

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

View File

@ -1,25 +1,25 @@
// 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;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class InfoMessage : Message
{
private static int infoID = -1;
public InfoMessage(string message) : base(infoID--)
{
Timestamp = DateTimeOffset.Now;
Content = message;
Sender = new User
{
Username = @"system",
Colour = @"0000ff",
};
}
}
}
// 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;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class InfoMessage : Message
{
private static int infoID = -1;
public InfoMessage(string message) : base(infoID--)
{
Timestamp = DateTimeOffset.Now;
Content = message;
Sender = new User
{
Username = @"system",
Colour = @"0000ff",
};
}
}
}

View File

@ -1,12 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : Message
{
public LocalEchoMessage() : base(null)
{
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Online.Chat
{
public class LocalEchoMessage : Message
{
public LocalEchoMessage() : base(null)
{
}
}
}

View File

@ -1,83 +1,83 @@
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class Message : IComparable<Message>, IEquatable<Message>
{
[JsonProperty(@"message_id")]
public readonly long? Id;
//todo: this should be inside sender.
[JsonProperty(@"sender_id")]
public int UserId;
[JsonProperty(@"target_type")]
public TargetType TargetType;
[JsonProperty(@"target_id")]
public long TargetId;
[JsonProperty(@"is_action")]
public bool IsAction;
[JsonProperty(@"timestamp")]
public DateTimeOffset Timestamp;
[JsonProperty(@"content")]
public string Content;
[JsonProperty(@"sender")]
public User Sender;
[JsonConstructor]
public Message()
{
}
/// <summary>
/// The text that is displayed in chat.
/// </summary>
public string DisplayContent { get; set; }
/// <summary>
/// The links found in this message.
/// </summary>
/// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks>
public List<Link> Links;
public Message(long? id)
{
Id = id;
}
public int CompareTo(Message other)
{
if (!Id.HasValue)
return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp);
if (!other.Id.HasValue)
return -1;
return Id.Value.CompareTo(other.Id.Value);
}
public virtual bool Equals(Message other) => Id == other?.Id;
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
}
public enum TargetType
{
[Description(@"channel")]
Channel,
[Description(@"user")]
User
}
}
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class Message : IComparable<Message>, IEquatable<Message>
{
[JsonProperty(@"message_id")]
public readonly long? Id;
//todo: this should be inside sender.
[JsonProperty(@"sender_id")]
public int UserId;
[JsonProperty(@"target_type")]
public TargetType TargetType;
[JsonProperty(@"target_id")]
public long TargetId;
[JsonProperty(@"is_action")]
public bool IsAction;
[JsonProperty(@"timestamp")]
public DateTimeOffset Timestamp;
[JsonProperty(@"content")]
public string Content;
[JsonProperty(@"sender")]
public User Sender;
[JsonConstructor]
public Message()
{
}
/// <summary>
/// The text that is displayed in chat.
/// </summary>
public string DisplayContent { get; set; }
/// <summary>
/// The links found in this message.
/// </summary>
/// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks>
public List<Link> Links;
public Message(long? id)
{
Id = id;
}
public int CompareTo(Message other)
{
if (!Id.HasValue)
return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp);
if (!other.Id.HasValue)
return -1;
return Id.Value.CompareTo(other.Id.Value);
}
public virtual bool Equals(Message other) => Id == other?.Id;
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
}
public enum TargetType
{
[Description(@"channel")]
Channel,
[Description(@"user")]
User
}
}

View File

@ -1,278 +1,278 @@
// 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;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace osu.Game.Online.Chat
{
public static class MessageFormatter
{
// [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points)
private static readonly Regex wiki_regex = new Regex(@"\[\[([^\]]+)\]\]");
// (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234)
private static readonly Regex old_link_regex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]");
// [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234)
private static readonly Regex new_link_regex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?<open>\[)[^\[\]]*)+((?<close-open>\])[^\[\]]*)+)*(?(open)(?!)))\]");
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
private static readonly Regex markdown_link_regex = new Regex(@"\[([^\]]*)\]\(([a-z]+://[^ ]+)\)");
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
// 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);
// 00:00:000 (1,2,3) - test
private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*");
// #osu
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
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();
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();
if (displayText.Length == 0 || linkText.Length == 0) continue;
// Check for encapsulated links
if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null)
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
}
}
}
private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0)
{
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
var link = m.Groups["link"].Value;
var indexLength = link.Length;
var details = getLinkDetails(link);
result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
}
}
private static LinkDetails getLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
{
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh"))
{
switch (args[2])
{
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);
}
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
{
var result = new MessageFormatterResult(toFormat);
// handle the [link display] format
handleMatches(new_link_regex, "{2}", "{1}", result, startIndex);
// handle the standard markdown []() format
handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex);
// handle the ()[] link format
handleMatches(old_link_regex, "{1}", "{2}", result, startIndex);
// handle wiki links
handleMatches(wiki_regex, "{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex);
// handle bare links
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel);
var empty = "";
while (space-- > 0)
empty += "\0";
handleMatches(emoji_regex, empty, "{0}", result, startIndex);
return result;
}
public static Message FormatMessage(Message inputMessage)
{
var result = format(inputMessage.Content);
inputMessage.DisplayContent = result.Text;
// Sometimes, regex matches are not in order
result.Links.Sort();
inputMessage.Links = result.Links;
return inputMessage;
}
public static MessageFormatterResult FormatText(string text)
{
var result = format(text);
result.Links.Sort();
return result;
}
public class MessageFormatterResult
{
public List<Link> Links = new List<Link>();
public string Text;
public string OriginalText;
public MessageFormatterResult(string text)
{
OriginalText = Text = text;
}
}
public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
}
}
public enum LinkAction
{
External,
OpenBeatmap,
OpenBeatmapSet,
OpenChannel,
OpenEditorTimestamp,
JoinMultiplayerMatch,
Spectate,
OpenUserProfile,
}
public class Link : IComparable<Link>
{
public string Url;
public int Index;
public int Length;
public LinkAction Action;
public string Argument;
public Link(string url, int startIndex, int length, LinkAction action, string argument)
{
Url = url;
Index = startIndex;
Length = length;
Action = action;
Argument = argument;
}
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
}
}
// 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;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace osu.Game.Online.Chat
{
public static class MessageFormatter
{
// [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points)
private static readonly Regex wiki_regex = new Regex(@"\[\[([^\]]+)\]\]");
// (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234)
private static readonly Regex old_link_regex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]");
// [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234)
private static readonly Regex new_link_regex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?<open>\[)[^\[\]]*)+((?<close-open>\])[^\[\]]*)+)*(?(open)(?!)))\]");
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
private static readonly Regex markdown_link_regex = new Regex(@"\[([^\]]*)\]\(([a-z]+://[^ ]+)\)");
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
// 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);
// 00:00:000 (1,2,3) - test
private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*");
// #osu
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
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();
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();
if (displayText.Length == 0 || linkText.Length == 0) continue;
// Check for encapsulated links
if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null)
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
}
}
}
private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0)
{
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
var link = m.Groups["link"].Value;
var indexLength = link.Length;
var details = getLinkDetails(link);
result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
}
}
private static LinkDetails getLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
{
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh"))
{
switch (args[2])
{
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);
}
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
{
var result = new MessageFormatterResult(toFormat);
// handle the [link display] format
handleMatches(new_link_regex, "{2}", "{1}", result, startIndex);
// handle the standard markdown []() format
handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex);
// handle the ()[] link format
handleMatches(old_link_regex, "{1}", "{2}", result, startIndex);
// handle wiki links
handleMatches(wiki_regex, "{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex);
// handle bare links
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel);
var empty = "";
while (space-- > 0)
empty += "\0";
handleMatches(emoji_regex, empty, "{0}", result, startIndex);
return result;
}
public static Message FormatMessage(Message inputMessage)
{
var result = format(inputMessage.Content);
inputMessage.DisplayContent = result.Text;
// Sometimes, regex matches are not in order
result.Links.Sort();
inputMessage.Links = result.Links;
return inputMessage;
}
public static MessageFormatterResult FormatText(string text)
{
var result = format(text);
result.Links.Sort();
return result;
}
public class MessageFormatterResult
{
public List<Link> Links = new List<Link>();
public string Text;
public string OriginalText;
public MessageFormatterResult(string text)
{
OriginalText = Text = text;
}
}
public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
}
}
public enum LinkAction
{
External,
OpenBeatmap,
OpenBeatmapSet,
OpenChannel,
OpenEditorTimestamp,
JoinMultiplayerMatch,
Spectate,
OpenUserProfile,
}
public class Link : IComparable<Link>
{
public string Url;
public int Index;
public int Length;
public LinkAction Action;
public string Argument;
public Link(string url, int startIndex, int length, LinkAction action, string argument)
{
Url = url;
Index = startIndex;
Length = length;
Action = action;
Argument = argument;
}
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
}
}

View File

@ -1,146 +1,146 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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
{
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),
},
},
},
};
}
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
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
{
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),
},
},
},
};
}
}
}

View File

@ -1,20 +1,20 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Beatmaps;
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<GameType> Type = new Bindable<GameType>();
public Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
public Bindable<int?> MaxParticipants = new Bindable<int?>();
public Bindable<User[]> Participants = new Bindable<User[]>();
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Beatmaps;
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<GameType> Type = new Bindable<GameType>();
public Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
public Bindable<int?> MaxParticipants = new Bindable<int?>();
public Bindable<User[]> Participants = new Bindable<User[]>();
}
}

View File

@ -1,26 +1,26 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Game.Graphics;
namespace osu.Game.Online.Multiplayer
{
public abstract class RoomStatus
{
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;
}
}
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Game.Graphics;
namespace osu.Game.Online.Multiplayer
{
public abstract class RoomStatus
{
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;
}
}