diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml b/.idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml
similarity index 73%
rename from .idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml
rename to .idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml
index 2eff16cc91..6463dd6ea5 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/CatchRuleset__Tests_.xml
@@ -1,18 +1,21 @@
-
+
+
-
+
-
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml b/.idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml
similarity index 73%
rename from .idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml
rename to .idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml
index cae9754560..0b63b2d966 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/ManiaRuleset__Tests_.xml
@@ -1,18 +1,21 @@
-
+
+
-
+
-
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml b/.idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml
similarity index 72%
rename from .idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml
rename to .idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml
index 49ec93e1b3..750ece648b 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/OsuRuleset__Tests_.xml
@@ -1,18 +1,21 @@
-
+
+
-
+
-
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml b/.idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml
similarity index 73%
rename from .idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml
rename to .idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml
index d0964c6f68..7b359a1ca0 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/TaikoRuleset__Tests_.xml
@@ -1,18 +1,21 @@
-
+
+
-
+
-
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu/.idea/runConfigurations/Tournament.xml
new file mode 100644
index 0000000000..3722f3dc04
--- /dev/null
+++ b/.idea/.idea.osu/.idea/runConfigurations/Tournament.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml
new file mode 100644
index 0000000000..e2628a1bb4
--- /dev/null
+++ b/.idea/.idea.osu/.idea/runConfigurations/Tournament__Tests_.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
index 2735f4ceb3..7ac6bb745d 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/.idea/.idea.osu/.idea/runConfigurations/VisualTests.xml b/.idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml
similarity index 70%
rename from .idea/.idea.osu/.idea/runConfigurations/VisualTests.xml
rename to .idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml
index 95cb4c0e62..7fcb7c15ea 100644
--- a/.idea/.idea.osu/.idea/runConfigurations/VisualTests.xml
+++ b/.idea/.idea.osu/.idea/runConfigurations/osu___Tests_.xml
@@ -1,17 +1,20 @@
-
+
+
-
-
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index c3306c2db7..57ff3e6b43 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,41 +1,6 @@
{
"version": "0.2.0",
- "configurations": [
- {
- "name": "VisualTests (Debug)",
- "type": "coreclr",
- "request": "launch",
- "program": "dotnet",
- "args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tests.dll"
- ],
- "cwd": "${workspaceRoot}",
- "preLaunchTask": "Build tests (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
- }
- },
- "console": "internalConsole"
- },
- {
- "name": "VisualTests (Release)",
- "type": "coreclr",
- "request": "launch",
- "program": "dotnet",
- "args": [
- "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2/osu.Game.Tests.dll"
- ],
- "cwd": "${workspaceRoot}",
- "preLaunchTask": "Build tests (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
- }
- },
- "console": "internalConsole"
- },
- {
+ "configurations": [{
"name": "osu! (Debug)",
"type": "coreclr",
"request": "launch",
@@ -69,6 +34,111 @@
},
"console": "internalConsole"
},
+ {
+ "name": "osu! (Tests, Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build tests (Debug)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ }, {
+ "name": "osu! (Tests, Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2/osu.Game.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build tests (Release)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ },
+ {
+ "name": "Tournament (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2/osu!.dll",
+ "--tournament"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build osu! (Debug)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ },
+ {
+ "name": "Tournament (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2/osu!.dll",
+ "--tournament"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build osu! (Release)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ },
+ {
+ "name": "Tournament (Tests, Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tournament.Tests.dll",
+ "--tournament"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build tournament tests (Debug)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ },
+ {
+ "name": "Tournament (Tests, Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2/osu.Game.Tournament.Tests.dll",
+ "--tournament"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build tournament tests (Release)",
+ "linux": {
+ "env": {
+ "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp2.2:${env:LD_LIBRARY_PATH}"
+ }
+ },
+ "console": "internalConsole"
+ },
{
"name": "Cake: Debug Script",
"type": "coreclr",
@@ -84,4 +154,4 @@
"externalConsole": false
}
]
-}
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index de799a7c03..aba590f466 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -2,8 +2,7 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
- "tasks": [
- {
+ "tasks": [{
"label": "Build osu! (Debug)",
"type": "shell",
"command": "dotnet",
@@ -65,6 +64,36 @@
"group": "build",
"problemMatcher": "$msCompile"
},
+ {
+ "label": "Build tournament tests (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Tournament.Tests",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ }, {
+ "label": "Build tournament tests (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Tournament.Tests",
+ "/p:Configuration=Release",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
{
"label": "Restore (netcoreapp2.2)",
"type": "shell",
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 29554df64c..cb488fea52 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -11,6 +11,7 @@ using osu.Framework.Development;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
+using osu.Game.Tournament;
namespace osu.Desktop
{
@@ -46,6 +47,10 @@ namespace osu.Desktop
default:
host.Run(new OsuGameDesktop(args));
break;
+
+ case "--tournament":
+ host.Run(new TournamentGame());
+ break;
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index aa8848c55f..8c9e1f279f 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -17,6 +17,7 @@
osu.Desktop.Program
+
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyConfiguration.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
similarity index 50%
rename from osu.Game.Tests/Visual/Settings/TestSceneKeyConfiguration.cs
rename to osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index d06d82ddb5..426ff988c4 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyConfiguration.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -1,17 +1,30 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Overlays;
+using osu.Game.Overlays.KeyBinding;
namespace osu.Game.Tests.Visual.Settings
{
[TestFixture]
- public class TestSceneKeyConfiguration : OsuTestScene
+ public class TestSceneKeyBindingPanel : OsuTestScene
{
private readonly KeyBindingPanel panel;
- public TestSceneKeyConfiguration()
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(KeyBindingRow),
+ typeof(GlobalKeyBindingsSection),
+ typeof(KeyBindingRow),
+ typeof(KeyBindingsSubsection),
+ typeof(RulesetBindingsSection),
+ typeof(VariantBindingsSubsection),
+ };
+
+ public TestSceneKeyBindingPanel()
{
Child = panel = new KeyBindingPanel();
}
diff --git a/osu.Game.Tests/Visual/Tournament/TestSceneDrawings.cs b/osu.Game.Tests/Visual/Tournament/TestSceneDrawings.cs
deleted file mode 100644
index 995819f7ae..0000000000
--- a/osu.Game.Tests/Visual/Tournament/TestSceneDrawings.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using System.ComponentModel;
-using osu.Framework.Allocation;
-using osu.Game.Screens.Tournament;
-using osu.Game.Screens.Tournament.Teams;
-
-namespace osu.Game.Tests.Visual.Tournament
-{
- [Description("for tournament use")]
- public class TestSceneDrawings : ScreenTestScene
- {
- [BackgroundDependencyLoader]
- private void load()
- {
- LoadScreen(new Drawings
- {
- TeamList = new TestTeamList(),
- });
- }
-
- private class TestTeamList : ITeamList
- {
- public IEnumerable Teams { get; } = new[]
- {
- new DrawingsTeam
- {
- FlagName = "GB",
- FullName = "United Kingdom",
- Acronym = "UK"
- },
- new DrawingsTeam
- {
- FlagName = "FR",
- FullName = "France",
- Acronym = "FRA"
- },
- new DrawingsTeam
- {
- FlagName = "CN",
- FullName = "China",
- Acronym = "CHN"
- },
- new DrawingsTeam
- {
- FlagName = "AU",
- FullName = "Australia",
- Acronym = "AUS"
- },
- new DrawingsTeam
- {
- FlagName = "JP",
- FullName = "Japan",
- Acronym = "JPN"
- },
- new DrawingsTeam
- {
- FlagName = "RO",
- FullName = "Romania",
- Acronym = "ROM"
- },
- new DrawingsTeam
- {
- FlagName = "IT",
- FullName = "Italy",
- Acronym = "PIZZA"
- },
- new DrawingsTeam
- {
- FlagName = "VE",
- FullName = "Venezuela",
- Acronym = "VNZ"
- },
- new DrawingsTeam
- {
- FlagName = "US",
- FullName = "United States of America",
- Acronym = "USA"
- },
- };
- }
- }
-}
diff --git a/osu.Game.Tournament.Tests/.vscode/launch.json b/osu.Game.Tournament.Tests/.vscode/launch.json
new file mode 100644
index 0000000000..0204158347
--- /dev/null
+++ b/osu.Game.Tournament.Tests/.vscode/launch.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "VisualTests (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Tournament.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Debug)",
+ "env": {},
+ "console": "internalConsole"
+ },
+ {
+ "name": "VisualTests (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "dotnet",
+ "args": [
+ "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Tournament.Tests.dll"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build (Release)",
+ "env": {},
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Tournament.Tests/.vscode/tasks.json b/osu.Game.Tournament.Tests/.vscode/tasks.json
new file mode 100644
index 0000000000..37f2f32874
--- /dev/null
+++ b/osu.Game.Tournament.Tests/.vscode/tasks.json
@@ -0,0 +1,47 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Tournament.Tests.csproj",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Tournament.Tests.csproj",
+ "/p:Configuration=Release",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Restore",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "restore"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs
new file mode 100644
index 0000000000..f329623703
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs
@@ -0,0 +1,93 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Ladder.Components;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneDrawableTournamentMatch : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(TournamentMatch),
+ typeof(DrawableTournamentTeam),
+ };
+
+ public TestSceneDrawableTournamentMatch()
+ {
+ Container level1;
+ Container level2;
+
+ var match1 = new TournamentMatch(
+ new TournamentTeam { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, },
+ new TournamentTeam { FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, Acronym = { Value = "JPN" } })
+ {
+ Team1Score = { Value = 4 },
+ Team2Score = { Value = 1 },
+ };
+
+ var match2 = new TournamentMatch(
+ new TournamentTeam
+ {
+ FlagName = { Value = "RO" },
+ FullName = { Value = "Romania" },
+ }
+ );
+
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ level1 = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new[]
+ {
+ new DrawableTournamentMatch(match1),
+ new DrawableTournamentMatch(match2),
+ new DrawableTournamentMatch(new TournamentMatch()),
+ }
+ },
+ level2 = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Margin = new MarginPadding(20),
+ Children = new[]
+ {
+ new DrawableTournamentMatch(new TournamentMatch()),
+ new DrawableTournamentMatch(new TournamentMatch())
+ }
+ }
+ }
+ };
+
+ level1.Children[0].Match.Progression.Value = level2.Children[0].Match;
+ level1.Children[1].Match.Progression.Value = level2.Children[0].Match;
+
+ AddRepeatStep("change scores", () => match1.Team2Score.Value++, 4);
+ AddStep("add new team", () => match2.Team2.Value = new TournamentTeam { FlagName = { Value = "PT" }, FullName = { Value = "Portugal" } });
+ AddStep("Add progression", () => level1.Children[2].Match.Progression.Value = level2.Children[1].Match);
+
+ AddStep("start match", () => match2.StartMatch());
+
+ AddRepeatStep("change scores", () => match2.Team1Score.Value++, 10);
+
+ AddStep("start submatch", () => level2.Children[0].Match.StartMatch());
+
+ AddRepeatStep("change scores", () => level2.Children[0].Match.Team1Score.Value++, 5);
+
+ AddRepeatStep("change scores", () => level2.Children[0].Match.Team2Score.Value++, 4);
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs
new file mode 100644
index 0000000000..72d9eb0e07
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.MathUtils;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Screens.Gameplay.Components;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneMatchScoreDisplay : LadderTestScene
+ {
+ [Cached(Type = typeof(MatchIPCInfo))]
+ private MatchIPCInfo matchInfo = new MatchIPCInfo();
+
+ public TestSceneMatchScoreDisplay()
+ {
+ Add(new MatchScoreDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Scheduler.AddDelayed(() =>
+ {
+ int amount = (int)((RNG.NextDouble() - 0.5) * 10000);
+ if (amount < 0)
+ matchInfo.Score1.Value -= amount;
+ else
+ matchInfo.Score2.Value += amount;
+ }, 100, true);
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs
new file mode 100644
index 0000000000..77fa411058
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Components;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneTournamentBeatmapPanel : OsuTestScene
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 1091460 });
+ req.Success += success;
+ api.Queue(req);
+ }
+
+ private void success(APIBeatmap apiBeatmap)
+ {
+ var beatmap = apiBeatmap.ToBeatmap(rulesets);
+ Add(new TournamentBeatmapPanel(beatmap)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs
new file mode 100644
index 0000000000..41d32d9448
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Online.Chat;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Models;
+using osu.Game.Users;
+
+namespace osu.Game.Tournament.Tests.Components
+{
+ public class TestSceneTournamentMatchChatDisplay : OsuTestScene
+ {
+ private readonly Channel testChannel = new Channel();
+ private readonly Channel testChannel2 = new Channel();
+
+ private readonly User admin = new User
+ {
+ Username = "HappyStick",
+ Id = 2,
+ Colour = "f2ca34"
+ };
+
+ private readonly User redUser = new User
+ {
+ Username = "BanchoBot",
+ Id = 3,
+ };
+
+ private readonly User blueUser = new User
+ {
+ Username = "Zallius",
+ Id = 4,
+ };
+
+ [Cached]
+ private LadderInfo ladderInfo = new LadderInfo();
+
+ [Cached]
+ private MatchIPCInfo matchInfo = new MatchIPCInfo(); // hide parent
+
+ private readonly TournamentMatchChatDisplay chatDisplay;
+
+ public TestSceneTournamentMatchChatDisplay()
+ {
+ Add(chatDisplay = new TournamentMatchChatDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ ladderInfo.CurrentMatch.Value = new TournamentMatch
+ {
+ Team1 =
+ {
+ Value = new TournamentTeam { Players = new BindableList { redUser } }
+ },
+ Team2 =
+ {
+ Value = new TournamentTeam { Players = new BindableList { blueUser } }
+ }
+ };
+
+ chatDisplay.Channel.Value = testChannel;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = admin,
+ Content = "I am a wang!"
+ }));
+
+ AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = redUser,
+ Content = "I am team red."
+ }));
+
+ AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = redUser,
+ Content = "I plan to win!"
+ }));
+
+ AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = blueUser,
+ Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand."
+ }));
+
+ AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = admin,
+ Content = "Okay okay, calm down guys. Let's do this!"
+ }));
+
+ AddStep("multiple messages", () => testChannel.AddNewMessages(new Message(nextMessageId())
+ {
+ Sender = admin,
+ Content = "I spam you!"
+ },
+ new Message(nextMessageId())
+ {
+ Sender = admin,
+ Content = "I spam you!!!1"
+ },
+ new Message(nextMessageId())
+ {
+ Sender = admin,
+ Content = "I spam you!1!1"
+ }));
+
+ AddStep("change channel to 2", () => chatDisplay.Channel.Value = testChannel2);
+
+ AddStep("change channel to 1", () => chatDisplay.Channel.Value = testChannel);
+ }
+
+ private int messageId;
+
+ private long? nextMessageId() => messageId++;
+ }
+}
diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs
new file mode 100644
index 0000000000..b49341d0d1
--- /dev/null
+++ b/osu.Game.Tournament.Tests/LadderTestScene.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.Tests
+{
+ public abstract class LadderTestScene : OsuTestScene
+ {
+ [Resolved]
+ protected LadderInfo Ladder { get; private set; }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs
new file mode 100644
index 0000000000..201736f38a
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Screens.Gameplay;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneGameplayScreen : OsuTestScene
+ {
+ [Cached]
+ private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new GameplayScreen());
+ Add(chat);
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs
new file mode 100644
index 0000000000..a45c5de2bd
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Cursor;
+using osu.Game.Tournament.Screens.Editors;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneLadderEditorScreen : LadderTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new OsuContextMenuContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new LadderEditorScreen()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs
new file mode 100644
index 0000000000..2be0564c82
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Cursor;
+using osu.Game.Tournament.Screens.Ladder;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneLadderScreen : LadderTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new OsuContextMenuContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new LadderScreen()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
new file mode 100644
index 0000000000..a7011c6d3c
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Game.Tournament.Screens.MapPool;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneMapPoolScreen : LadderTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(MapPoolScreen)
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new MapPoolScreen { Width = 0.7f });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs
new file mode 100644
index 0000000000..e15ac416b0
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tournament.Screens.Editors;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneRoundEditorScreen : LadderTestScene
+ {
+ public TestSceneRoundEditorScreen()
+ {
+ Add(new RoundEditorScreen
+ {
+ Width = 0.85f // create room for control panel
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs
new file mode 100644
index 0000000000..f3e65919eb
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Screens.Schedule;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneScheduleScreen : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new ScheduleScreen());
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs
new file mode 100644
index 0000000000..edf1477b06
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Tests.Visual;
+using osu.Game.Tournament.Screens.Showcase;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneShowcaseScreen : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new ShowcaseScreen());
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs
new file mode 100644
index 0000000000..097bad4a02
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tournament.Screens.Editors;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneTeamEditorScreen : LadderTestScene
+ {
+ public TestSceneTeamEditorScreen()
+ {
+ Add(new TeamEditorScreen
+ {
+ Width = 0.85f // create room for control panel
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs
new file mode 100644
index 0000000000..3d340e393c
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.TeamIntro;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneTeamIntroScreen : LadderTestScene
+ {
+ [Cached]
+ private readonly Bindable currentMatch = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var match = new TournamentMatch();
+ match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
+ match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
+ match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
+ currentMatch.Value = match;
+
+ Add(new TeamIntroScreen
+ {
+ FillMode = FillMode.Fit,
+ FillAspectRatio = 16 / 9f
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs
new file mode 100644
index 0000000000..6f5e17a36e
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.TeamWin;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneTeamWinScreen : LadderTestScene
+ {
+ [Cached]
+ private readonly Bindable currentMatch = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var match = new TournamentMatch();
+ match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
+ match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
+ match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
+ currentMatch.Value = match;
+
+ Add(new TeamWinScreen
+ {
+ FillMode = FillMode.Fit,
+ FillAspectRatio = 16 / 9f
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs
new file mode 100644
index 0000000000..378614343a
--- /dev/null
+++ b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Platform;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tournament.Tests
+{
+ public class TestSceneTournamentSceneManager : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(Storage storage)
+ {
+ Add(new TournamentSceneManager());
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs
new file mode 100644
index 0000000000..f7ad757926
--- /dev/null
+++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Testing;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Backgrounds;
+
+namespace osu.Game.Tournament.Tests
+{
+ public class TournamentTestBrowser : TournamentGameBase
+ {
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ LoadComponentAsync(new Background("Menu/menu-background-0")
+ {
+ Colour = OsuColour.Gray(0.5f),
+ Depth = 10
+ }, AddInternal);
+
+ // Have to construct this here, rather than in the constructor, because
+ // we depend on some dependencies to be loaded within OsuGameBase.load().
+ Add(new TestBrowser());
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs
new file mode 100644
index 0000000000..1f63f7c545
--- /dev/null
+++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework;
+using osu.Framework.Platform;
+
+namespace osu.Game.Tournament.Tests
+{
+ public static class TournamentTestRunner
+ {
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ {
+ host.Run(new TournamentTestBrowser());
+ return 0;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
new file mode 100644
index 0000000000..1c169184fb
--- /dev/null
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ osu.Game.Tournament.Tests.TournamentTestRunner
+
+
+
+
+
+
+
+
+ WinExe
+ netcoreapp2.2
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs
new file mode 100644
index 0000000000..a9bb1bf42f
--- /dev/null
+++ b/osu.Game.Tournament/Components/ControlPanel.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Components
+{
+ ///
+ /// An element anchored to the right-hand area of a screen that provides streamer level controls.
+ /// Should be off-screen.
+ ///
+ public class ControlPanel : Container
+ {
+ private readonly FillFlowContainer buttons;
+
+ protected override Container Content => buttons;
+
+ public ControlPanel()
+ {
+ RelativeSizeAxes = Axes.Both;
+ AlwaysPresent = true;
+ Width = 0.15f;
+ Anchor = Anchor.TopRight;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(54, 54, 54, 255)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = "Control Panel",
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22)
+ },
+ buttons = new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Width = 0.75f,
+ Position = new Vector2(0, 35f),
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5f),
+ },
+ };
+ }
+
+ public class Spacer : CompositeDrawable
+ {
+ public Spacer(float height = 20)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+ AlwaysPresent = true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs
new file mode 100644
index 0000000000..ee7e350970
--- /dev/null
+++ b/osu.Game.Tournament/Components/DateTextBox.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Tournament.Components
+{
+ public class DateTextBox : SettingsTextBox
+ {
+ public new Bindable Bindable
+ {
+ get => bindable;
+ set
+ {
+ bindable = value.GetBoundCopy();
+ bindable.BindValueChanged(dto =>
+ base.Bindable.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true);
+ }
+ }
+
+ // hold a reference to the provided bindable so we don't have to in every settings section.
+ private Bindable bindable;
+
+ public DateTextBox()
+ {
+ base.Bindable = new Bindable();
+ ((OsuTextBox)Control).OnCommit = (sender, newText) =>
+ {
+ try
+ {
+ bindable.Value = DateTimeOffset.Parse(sender.Text);
+ }
+ catch
+ {
+ // reset textbox content to its last valid state on a parse failure.
+ bindable.TriggerChange();
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs
new file mode 100644
index 0000000000..361bd92770
--- /dev/null
+++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.Components
+{
+ public abstract class DrawableTournamentTeam : CompositeDrawable
+ {
+ public readonly TournamentTeam Team;
+
+ protected readonly Sprite Flag;
+ protected readonly OsuSpriteText AcronymText;
+
+ [UsedImplicitly]
+ private Bindable acronym;
+
+ [UsedImplicitly]
+ private Bindable flag;
+
+ protected DrawableTournamentTeam(TournamentTeam team)
+ {
+ Team = team;
+
+ Flag = new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit
+ };
+
+ AcronymText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(weight: FontWeight.Regular),
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ if (Team == null) return;
+
+ (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(acronym => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true);
+ (flag = Team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Flag.Texture = textures.Get($@"Flags/{Team.FlagName}"), true);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
new file mode 100644
index 0000000000..c07882ddd0
--- /dev/null
+++ b/osu.Game.Tournament/Components/SongBar.cs
@@ -0,0 +1,251 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Screens.Menu;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Components
+{
+ public class SongBar : CompositeDrawable
+ {
+ private BeatmapInfo beatmap;
+
+ public BeatmapInfo Beatmap
+ {
+ get => beatmap;
+ set
+ {
+ if (beatmap == value)
+ return;
+
+ beatmap = value;
+ update();
+ }
+ }
+
+ private LegacyMods mods;
+
+ public LegacyMods Mods
+ {
+ get => mods;
+ set
+ {
+ mods = value;
+ update();
+ }
+ }
+
+ private Container panelContents;
+ private Container innerPanel;
+ private Container outerPanel;
+ private TournamentBeatmapPanel panel;
+
+ private float panelWidth => expanded ? 0.6f : 1;
+
+ private const float main_width = 0.97f;
+ private const float inner_panel_width = 0.7f;
+
+ private bool expanded;
+
+ public bool Expanded
+ {
+ get => expanded;
+ set
+ {
+ expanded = value;
+ panel?.ResizeWidthTo(panelWidth, 800, Easing.OutQuint);
+
+ if (expanded)
+ {
+ innerPanel.ResizeWidthTo(inner_panel_width, 800, Easing.OutQuint);
+ outerPanel.ResizeWidthTo(main_width, 800, Easing.OutQuint);
+ }
+ else
+ {
+ innerPanel.ResizeWidthTo(1, 800, Easing.OutQuint);
+ outerPanel.ResizeWidthTo(0.25f, 800, Easing.OutQuint);
+ }
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ outerPanel = new Container
+ {
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.2f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 5,
+ },
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ RelativePositionAxes = Axes.X,
+ X = -(1 - main_width) / 2,
+ Y = -10,
+ Width = main_width,
+ Height = TournamentBeatmapPanel.HEIGHT,
+ CornerRadius = TournamentBeatmapPanel.HEIGHT / 2,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.93f),
+ },
+ new OsuLogo
+ {
+ Triangles = false,
+ Colour = OsuColour.Gray(0.33f),
+ Scale = new Vector2(0.08f),
+ Margin = new MarginPadding(50),
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ },
+ innerPanel = new Container
+ {
+ Masking = true,
+ CornerRadius = TournamentBeatmapPanel.HEIGHT / 2,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = inner_panel_width,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.86f),
+ },
+ panelContents = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ }
+ }
+ }
+ };
+
+ Expanded = true;
+ }
+
+ private void update()
+ {
+ if (beatmap == null)
+ {
+ panelContents.Clear();
+ return;
+ }
+
+ var bpm = beatmap.BeatmapSet.OnlineInfo.BPM;
+ var length = beatmap.OnlineInfo.Length;
+ string hardRockExtra = "";
+ string srExtra = "";
+
+ //var ar = beatmap.BaseDifficulty.ApproachRate;
+ if ((mods & LegacyMods.HardRock) > 0)
+ {
+ hardRockExtra = "*";
+ srExtra = "*";
+ }
+
+ if ((mods & LegacyMods.DoubleTime) > 0)
+ {
+ //ar *= 1.5f;
+ bpm *= 1.5f;
+ length /= 1.5f;
+ srExtra = "*";
+ }
+
+ panelContents.Children = new Drawable[]
+ {
+ new DiffPiece(("Length", TimeSpan.FromSeconds(length).ToString(@"mm\:ss")))
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.BottomLeft,
+ },
+ new DiffPiece(("BPM", $"{bpm:0.#}"))
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.TopLeft
+ },
+ new DiffPiece(
+ //("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"),
+ //("AR", $"{ar:0.#}{srExtra}"),
+ ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"),
+ ("HP", $"{beatmap.BaseDifficulty.DrainRate:0.#}{hardRockExtra}")
+ )
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.BottomRight
+ },
+ new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}"))
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.TopRight
+ },
+ panel = new TournamentBeatmapPanel(beatmap)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(panelWidth, 1)
+ }
+ };
+ }
+
+ public class DiffPiece : TextFlowContainer
+ {
+ public DiffPiece(params (string heading, string content)[] tuples)
+ {
+ Margin = new MarginPadding { Horizontal = 15, Vertical = 1 };
+ AutoSizeAxes = Axes.Both;
+
+ void cp(SpriteText s, Color4 colour)
+ {
+ s.Colour = colour;
+ s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15);
+ }
+
+ for (var i = 0; i < tuples.Length; i++)
+ {
+ var tuple = tuples[i];
+
+ if (i > 0)
+ {
+ AddText(" / ", s =>
+ {
+ cp(s, OsuColour.Gray(0.33f));
+ s.Spacing = new Vector2(-2, 0);
+ });
+ }
+
+ AddText(new OsuSpriteText { Text = tuple.heading }, s => cp(s, OsuColour.Gray(0.33f)));
+ AddText(" ", s => cp(s, OsuColour.Gray(0.33f)));
+ AddText(new OsuSpriteText { Text = tuple.content }, s => cp(s, OsuColour.Gray(0.5f)));
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
new file mode 100644
index 0000000000..d5e28c1e3e
--- /dev/null
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -0,0 +1,203 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Tournament.Models;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Components
+{
+ public class TournamentBeatmapPanel : CompositeDrawable
+ {
+ public readonly BeatmapInfo Beatmap;
+ private readonly string mods;
+
+ private const float horizontal_padding = 10;
+ private const float vertical_padding = 5;
+
+ public const float HEIGHT = 50;
+
+ private readonly Bindable currentMatch = new Bindable();
+ private Box flash;
+
+ public TournamentBeatmapPanel(BeatmapInfo beatmap, string mods = null)
+ {
+ if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
+
+ Beatmap = beatmap;
+ this.mods = mods;
+ Width = 400;
+ Height = HEIGHT;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder, TextureStore textures)
+ {
+ currentMatch.BindValueChanged(matchChanged);
+ currentMatch.BindTo(ladder.CurrentMatch);
+
+ CornerRadius = HEIGHT / 2;
+ Masking = true;
+
+ AddRangeInternal(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ },
+ new UpdateableBeatmapSetCover
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.5f),
+ BeatmapSet = Beatmap.BeatmapSet,
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Padding = new MarginPadding(vertical_padding),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = new LocalisedString((
+ $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}",
+ $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")),
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Padding = new MarginPadding(vertical_padding),
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = "mapper",
+ Padding = new MarginPadding { Right = 5 },
+ Font = OsuFont.GetFont(italics: true, weight: FontWeight.Regular, size: 14)
+ },
+ new OsuSpriteText
+ {
+ Text = Beatmap.Metadata.AuthorString,
+ Padding = new MarginPadding { Right = 20 },
+ Font = OsuFont.GetFont(italics: true, weight: FontWeight.Bold, size: 14)
+ },
+ new OsuSpriteText
+ {
+ Text = "difficulty",
+ Padding = new MarginPadding { Right = 5 },
+ Font = OsuFont.GetFont(italics: true, weight: FontWeight.Regular, size: 14)
+ },
+ new OsuSpriteText
+ {
+ Text = Beatmap.Version,
+ Font = OsuFont.GetFont(italics: true, weight: FontWeight.Bold, size: 14)
+ },
+ }
+ }
+ },
+ },
+ flash = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Gray,
+ Blending = BlendingMode.Additive,
+ Alpha = 0,
+ },
+ });
+
+ if (!string.IsNullOrEmpty(mods))
+ AddInternal(new Sprite
+ {
+ Texture = textures.Get($"mods/{mods}"),
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Margin = new MarginPadding(20),
+ Scale = new Vector2(0.5f)
+ });
+ }
+
+ private void matchChanged(ValueChangedEvent match)
+ {
+ if (match.OldValue != null)
+ match.OldValue.PicksBans.CollectionChanged -= picksBansOnCollectionChanged;
+ match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged;
+ updateState();
+ }
+
+ private void picksBansOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ => updateState();
+
+ private BeatmapChoice choice;
+
+ private void updateState()
+ {
+ var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineBeatmapID);
+
+ bool doFlash = found != choice;
+ choice = found;
+
+ if (found != null)
+ {
+ if (doFlash)
+ flash?.FadeOutFromOne(500).Loop(0, 10);
+
+ BorderThickness = 6;
+
+ switch (found.Team)
+ {
+ case TeamColour.Red:
+ BorderColour = Color4.Red;
+ break;
+
+ case TeamColour.Blue:
+ BorderColour = Color4.Blue;
+ break;
+ }
+
+ switch (found.Type)
+ {
+ case ChoiceType.Pick:
+ Colour = Color4.White;
+ Alpha = 1;
+ break;
+
+ case ChoiceType.Ban:
+ Colour = Color4.Gray;
+ Alpha = 0.5f;
+ break;
+ }
+ }
+ else
+ {
+ Colour = Color4.White;
+ BorderThickness = 0;
+ Alpha = 1;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
new file mode 100644
index 0000000000..48c5b9bd35
--- /dev/null
+++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
@@ -0,0 +1,93 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.Chat;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Models;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Components
+{
+ public class TournamentMatchChatDisplay : StandAloneChatDisplay
+ {
+ private readonly Bindable chatChannel = new Bindable();
+
+ private ChannelManager manager;
+
+ public TournamentMatchChatDisplay()
+ {
+ RelativeSizeAxes = Axes.X;
+ Y = 100;
+ Size = new Vector2(0.45f, 112);
+ Margin = new MarginPadding(10);
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(MatchIPCInfo ipc)
+ {
+ if (ipc != null)
+ {
+ chatChannel.BindTo(ipc.ChatChannel);
+ chatChannel.BindValueChanged(c =>
+ {
+ if (string.IsNullOrWhiteSpace(c.NewValue))
+ return;
+
+ int id = int.Parse(c.NewValue);
+
+ if (id <= 0) return;
+
+ if (manager == null)
+ {
+ AddInternal(manager = new ChannelManager());
+ Channel.BindTo(manager.CurrentChannel);
+ }
+
+ foreach (var ch in manager.JoinedChannels.ToList())
+ manager.LeaveChannel(ch);
+
+ var channel = new Channel
+ {
+ Id = id,
+ Type = ChannelType.Public
+ };
+
+ manager.JoinChannel(channel);
+ manager.CurrentChannel.Value = channel;
+ }, true);
+ }
+ }
+
+ protected override ChatLine CreateMessage(Message message) => new MatchMessage(message);
+
+ protected class MatchMessage : StandAloneMessage
+ {
+ public MatchMessage(Message message)
+ : base(message)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo info)
+ {
+ //if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id))
+ // ColourBox.Colour = red;
+ //else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id))
+ // ColourBox.Colour = blue;
+ //else if (Message.Sender.Colour != null)
+ // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour);
+ }
+
+ private readonly Color4 red = new Color4(186, 0, 18, 255);
+ private readonly Color4 blue = new Color4(17, 136, 170, 255);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
new file mode 100644
index 0000000000..4f4660f645
--- /dev/null
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Video;
+using osu.Game.Graphics;
+
+namespace osu.Game.Tournament.Components
+{
+ public class TourneyVideo : CompositeDrawable
+ {
+ private readonly VideoSprite video;
+
+ public TourneyVideo(Stream stream)
+ {
+ if (stream == null)
+ {
+ InternalChild = new Box
+ {
+ Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.3f), OsuColour.Gray(0.6f)),
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+ else
+ InternalChild = video = new VideoSprite(stream)
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ };
+ }
+
+ public bool Loop
+ {
+ set
+ {
+ if (video != null)
+ video.Loop = value;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
new file mode 100644
index 0000000000..4fd858bd12
--- /dev/null
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -0,0 +1,194 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.Win32;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Framework.Platform.Windows;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Rulesets;
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.IPC
+{
+ public class FileBasedIPC : MatchIPCInfo
+ {
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ [Resolved]
+ protected RulesetStore Rulesets { get; private set; }
+
+ private int lastBeatmapId;
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder, GameHost host)
+ {
+ StableStorage stable;
+
+ try
+ {
+ stable = new StableStorage(host as DesktopGameHost);
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Stable installation could not be found; disabling file based IPC");
+ return;
+ }
+
+ const string file_ipc_filename = "ipc.txt";
+ const string file_ipc_state_filename = "ipc-state.txt";
+ const string file_ipc_scores_filename = "ipc-scores.txt";
+ const string file_ipc_channel_filename = "ipc-channel.txt";
+
+ if (stable.Exists(file_ipc_filename))
+ Scheduler.AddDelayed(delegate
+ {
+ try
+ {
+ using (var stream = stable.GetStream(file_ipc_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ var beatmapId = int.Parse(sr.ReadLine());
+ var mods = int.Parse(sr.ReadLine());
+
+ if (lastBeatmapId != beatmapId)
+ {
+ lastBeatmapId = beatmapId;
+
+ var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null);
+
+ if (existing != null)
+ Beatmap.Value = existing.BeatmapInfo;
+ else
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
+ req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets);
+ API.Queue(req);
+ }
+ }
+
+ Mods.Value = (LegacyMods)mods;
+ }
+ }
+ catch
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = stable.GetStream(file_ipc_channel_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ ChatChannel.Value = sr.ReadLine();
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = stable.GetStream(file_ipc_state_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = stable.GetStream(file_ipc_scores_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ Score1.Value = int.Parse(sr.ReadLine());
+ Score2.Value = int.Parse(sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+ }, 250, true);
+ }
+
+ ///
+ /// A method of accessing an osu-stable install in a controlled fashion.
+ ///
+ private class StableStorage : WindowsStorage
+ {
+ protected override string LocateBasePath()
+ {
+ bool checkExists(string p)
+ {
+ return File.Exists(Path.Combine(p, "ipc.txt"));
+ }
+
+ string stableInstallPath = string.Empty;
+
+ try
+ {
+ try
+ {
+ stableInstallPath = "G:\\My Drive\\Main\\osu!tourney";
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = "G:\\My Drive\\Main\\osu!mappool";
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
+
+ try
+ {
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(String.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ return null;
+ }
+ finally
+ {
+ Logger.Log($"Stable path for tourney usage: {stableInstallPath}");
+ }
+ }
+
+ public StableStorage(DesktopGameHost host)
+ : base(string.Empty, host)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/IPC/MatchIPCInfo.cs b/osu.Game.Tournament/IPC/MatchIPCInfo.cs
new file mode 100644
index 0000000000..701258c6c7
--- /dev/null
+++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Legacy;
+
+namespace osu.Game.Tournament.IPC
+{
+ public class MatchIPCInfo : Component
+ {
+ public Bindable Beatmap { get; } = new Bindable();
+ public Bindable Mods { get; } = new Bindable();
+ public Bindable State { get; } = new Bindable();
+ public Bindable ChatChannel { get; } = new Bindable();
+ public BindableInt Score1 { get; } = new BindableInt();
+ public BindableInt Score2 { get; } = new BindableInt();
+ }
+}
diff --git a/osu.Game.Tournament/IPC/TourneyState.cs b/osu.Game.Tournament/IPC/TourneyState.cs
new file mode 100644
index 0000000000..ef1c612a53
--- /dev/null
+++ b/osu.Game.Tournament/IPC/TourneyState.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Tournament.IPC
+{
+ public enum TourneyState
+ {
+ Initialising,
+ Idle,
+ WaitingForClients,
+ Playing,
+ Ranking
+ }
+}
diff --git a/osu.Game.Tournament/Models/BeatmapChoice.cs b/osu.Game.Tournament/Models/BeatmapChoice.cs
new file mode 100644
index 0000000000..384b349b24
--- /dev/null
+++ b/osu.Game.Tournament/Models/BeatmapChoice.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// A beatmap choice by a team from a tournament's map pool.
+ ///
+ [Serializable]
+ public class BeatmapChoice
+ {
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
+ public TeamColour Team;
+
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
+ public ChoiceType Type;
+
+ public int BeatmapID;
+ }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum TeamColour
+ {
+ Red,
+ Blue
+ }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum ChoiceType
+ {
+ Pick,
+ Ban,
+ }
+}
diff --git a/osu.Game.Tournament/Models/LadderEditorInfo.cs b/osu.Game.Tournament/Models/LadderEditorInfo.cs
new file mode 100644
index 0000000000..70fd115e25
--- /dev/null
+++ b/osu.Game.Tournament/Models/LadderEditorInfo.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+
+namespace osu.Game.Tournament.Models
+{
+ public class LadderEditorInfo
+ {
+ public readonly Bindable Selected = new Bindable();
+ }
+}
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
new file mode 100644
index 0000000000..547c4eab08
--- /dev/null
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// Holds the complete data required to operate the tournament system.
+ ///
+ [Serializable]
+ public class LadderInfo
+ {
+ public BindableList Matches = new BindableList();
+ public BindableList Rounds = new BindableList();
+ public BindableList Teams = new BindableList();
+
+ // only used for serialisation
+ public List Progressions = new List();
+
+ [JsonIgnore]
+ public Bindable CurrentMatch = new Bindable();
+ }
+}
diff --git a/osu.Game.Tournament/Models/RoundBeatmap.cs b/osu.Game.Tournament/Models/RoundBeatmap.cs
new file mode 100644
index 0000000000..5d43d0ca66
--- /dev/null
+++ b/osu.Game.Tournament/Models/RoundBeatmap.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Tournament.Models
+{
+ public class RoundBeatmap
+ {
+ public int ID;
+ public string Mods;
+
+ public BeatmapInfo BeatmapInfo;
+ }
+}
diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs
new file mode 100644
index 0000000000..06cce3d59e
--- /dev/null
+++ b/osu.Game.Tournament/Models/TournamentMatch.cs
@@ -0,0 +1,125 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+using osu.Game.Tournament.Screens.Ladder.Components;
+using SixLabors.Primitives;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// A collection of two teams competing in a head-to-head match.
+ ///
+ [Serializable]
+ public class TournamentMatch
+ {
+ public int ID;
+
+ public List Acronyms
+ {
+ get
+ {
+ List acronyms = new List();
+ if (Team1Acronym != null) acronyms.Add(Team1Acronym);
+ if (Team2Acronym != null) acronyms.Add(Team2Acronym);
+ return acronyms;
+ }
+ }
+
+ [JsonIgnore]
+ public readonly Bindable Team1 = new Bindable();
+
+ public string Team1Acronym;
+
+ public readonly Bindable Team1Score = new Bindable();
+
+ [JsonIgnore]
+ public readonly Bindable Team2 = new Bindable();
+
+ public string Team2Acronym;
+
+ public readonly Bindable Team2Score = new Bindable();
+
+ public readonly Bindable Completed = new Bindable();
+
+ public readonly Bindable Losers = new Bindable();
+
+ public readonly ObservableCollection PicksBans = new ObservableCollection();
+
+ [JsonIgnore]
+ public readonly Bindable Round = new Bindable();
+
+ [JsonIgnore]
+ public readonly Bindable Progression = new Bindable();
+
+ [JsonIgnore]
+ public readonly Bindable LosersProgression = new Bindable();
+
+ ///
+ /// Should not be set directly. Use LadderInfo.CurrentMatch.Value = this instead.
+ ///
+ public readonly Bindable Current = new Bindable();
+
+ public readonly Bindable Date = new Bindable(DateTimeOffset.Now);
+
+ [JsonProperty]
+ public readonly BindableList ConditionalMatches = new BindableList();
+
+ public readonly Bindable Position = new Bindable();
+
+ public TournamentMatch()
+ {
+ Team1.BindValueChanged(t => Team1Acronym = t.NewValue?.Acronym.Value, true);
+ Team2.BindValueChanged(t => Team2Acronym = t.NewValue?.Acronym.Value, true);
+ }
+
+ public TournamentMatch(TournamentTeam team1 = null, TournamentTeam team2 = null)
+ : this()
+ {
+ Team1.Value = team1;
+ Team2.Value = team2;
+ }
+
+ [JsonIgnore]
+ public TournamentTeam Winner => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team1.Value : Team2.Value;
+
+ [JsonIgnore]
+ public TournamentTeam Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value;
+
+ public int PointsToWin => Round.Value?.BestOf.Value / 2 + 1 ?? 0;
+
+ ///
+ /// Remove scores from the match, in case of a false click or false start.
+ ///
+ public void CancelMatchStart()
+ {
+ Team1Score.Value = null;
+ Team2Score.Value = null;
+ }
+
+ ///
+ /// Initialise this match with zeroed scores. Will be a noop if either team is not present.
+ ///
+ public void StartMatch()
+ {
+ if (Team1.Value == null || Team2.Value == null)
+ return;
+
+ Team1Score.Value = 0;
+ Team2Score.Value = 0;
+ }
+
+ public void Reset()
+ {
+ CancelMatchStart();
+ Team1.Value = null;
+ Team2.Value = null;
+ Completed.Value = false;
+ PicksBans.Clear();
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Models/TournamentProgression.cs b/osu.Game.Tournament/Models/TournamentProgression.cs
new file mode 100644
index 0000000000..5575b4c29e
--- /dev/null
+++ b/osu.Game.Tournament/Models/TournamentProgression.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// A mapping between two es.
+ /// Used for serialisation exclusively.
+ ///
+ [Serializable]
+ public class TournamentProgression
+ {
+ public int SourceID;
+ public int TargetID;
+
+ public bool Losers;
+
+ public TournamentProgression(int sourceID, int targetID, bool losers = false)
+ {
+ SourceID = sourceID;
+ TargetID = targetID;
+ Losers = losers;
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs
new file mode 100644
index 0000000000..08b3143be1
--- /dev/null
+++ b/osu.Game.Tournament/Models/TournamentRound.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// A tournament round, containing many matches, generally executed in a short time period.
+ ///
+ [Serializable]
+ public class TournamentRound
+ {
+ public readonly Bindable Name = new Bindable();
+ public readonly Bindable Description = new Bindable();
+
+ public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 };
+
+ [JsonProperty]
+ public readonly BindableList Beatmaps = new BindableList();
+
+ public readonly Bindable StartDate = new Bindable { Value = DateTimeOffset.UtcNow };
+
+ // only used for serialisation
+ public List Matches = new List();
+
+ public override string ToString() => Name.Value ?? "None";
+ }
+}
diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs
new file mode 100644
index 0000000000..54b8a35180
--- /dev/null
+++ b/osu.Game.Tournament/Models/TournamentTeam.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+using osu.Framework.Bindables;
+using osu.Game.Users;
+
+namespace osu.Game.Tournament.Models
+{
+ ///
+ /// A team representation. For official tournaments this is generally a country.
+ ///
+ [Serializable]
+ public class TournamentTeam
+ {
+ ///
+ /// The name of this team.
+ ///
+ public Bindable FullName = new Bindable(string.Empty);
+
+ ///
+ /// Name of the file containing the flag.
+ ///
+ public Bindable FlagName = new Bindable(string.Empty);
+
+ ///
+ /// Short acronym which appears in the group boxes post-selection.
+ ///
+ public Bindable Acronym = new Bindable(string.Empty);
+
+ [JsonProperty]
+ public BindableList Players { get; set; } = new BindableList();
+
+ public TournamentTeam()
+ {
+ Acronym.ValueChanged += val =>
+ {
+ // use a sane default flag name based on acronym.
+ if (val.OldValue.StartsWith(FlagName.Value, StringComparison.InvariantCultureIgnoreCase))
+ FlagName.Value = val.NewValue.Length >= 2 ? val.NewValue?.Substring(0, 2).ToUpper() : string.Empty;
+ };
+
+ FullName.ValueChanged += val =>
+ {
+ // use a sane acronym based on full name.
+ if (val.OldValue.StartsWith(Acronym.Value, StringComparison.InvariantCultureIgnoreCase))
+ Acronym.Value = val.NewValue.Length >= 3 ? val.NewValue?.Substring(0, 3).ToUpper() : string.Empty;
+ };
+ }
+
+ public override string ToString() => FullName.Value ?? Acronym.Value;
+ }
+}
diff --git a/osu.Game.Tournament/Properties/AssemblyInfo.cs b/osu.Game.Tournament/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..70e42bcafb
--- /dev/null
+++ b/osu.Game.Tournament/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Runtime.CompilerServices;
+
+// We publish our internal attributes to other sub-projects of the framework.
+// Note, that we omit visual tests as they are meant to test the framework
+// behavior "in the wild".
+
+[assembly: InternalsVisibleTo("osu.Game.Tournament.Tests")]
+[assembly: InternalsVisibleTo("osu.Game.Tournament.Tests.Dynamic")]
diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin
new file mode 100644
index 0000000000..42cfdf08de
Binary files /dev/null and b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin differ
diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png
new file mode 100644
index 0000000000..332d9ca056
Binary files /dev/null and b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png differ
diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin b/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin
new file mode 100644
index 0000000000..3047c2eb3e
Binary files /dev/null and b/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin differ
diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular_0.png b/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular_0.png
new file mode 100644
index 0000000000..1252d233d3
Binary files /dev/null and b/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular_0.png differ
diff --git a/osu.Game.Tournament/Resources/countries.json b/osu.Game.Tournament/Resources/countries.json
new file mode 100644
index 0000000000..ec2ca2bf37
--- /dev/null
+++ b/osu.Game.Tournament/Resources/countries.json
@@ -0,0 +1,1252 @@
+[
+ {
+ "FlagName": "BD",
+ "FullName": "Bangladesh",
+ "Acronym": "BGD"
+ },
+ {
+ "FlagName": "BE",
+ "FullName": "Belgium",
+ "Acronym": "BEL"
+ },
+ {
+ "FlagName": "BF",
+ "FullName": "Burkina Faso",
+ "Acronym": "BFA"
+ },
+ {
+ "FlagName": "BG",
+ "FullName": "Bulgaria",
+ "Acronym": "BGR"
+ },
+ {
+ "FlagName": "BA",
+ "FullName": "Bosnia and Herzegovina",
+ "Acronym": "BIH"
+ },
+ {
+ "FlagName": "BB",
+ "FullName": "Barbados",
+ "Acronym": "BRB"
+ },
+ {
+ "FlagName": "WF",
+ "FullName": "Wallis and Futuna",
+ "Acronym": "WLF"
+ },
+ {
+ "FlagName": "BL",
+ "FullName": "Saint Barthelemy",
+ "Acronym": "BLM"
+ },
+ {
+ "FlagName": "BM",
+ "FullName": "Bermuda",
+ "Acronym": "BMU"
+ },
+ {
+ "FlagName": "BN",
+ "FullName": "Brunei",
+ "Acronym": "BRN"
+ },
+ {
+ "FlagName": "BO",
+ "FullName": "Bolivia",
+ "Acronym": "BOL"
+ },
+ {
+ "FlagName": "BH",
+ "FullName": "Bahrain",
+ "Acronym": "BHR"
+ },
+ {
+ "FlagName": "BI",
+ "FullName": "Burundi",
+ "Acronym": "BDI"
+ },
+ {
+ "FlagName": "BJ",
+ "FullName": "Benin",
+ "Acronym": "BEN"
+ },
+ {
+ "FlagName": "BT",
+ "FullName": "Bhutan",
+ "Acronym": "BTN"
+ },
+ {
+ "FlagName": "JM",
+ "FullName": "Jamaica",
+ "Acronym": "JAM"
+ },
+ {
+ "FlagName": "BV",
+ "FullName": "Bouvet Island",
+ "Acronym": "BVT"
+ },
+ {
+ "FlagName": "BW",
+ "FullName": "Botswana",
+ "Acronym": "BWA"
+ },
+ {
+ "FlagName": "WS",
+ "FullName": "Samoa",
+ "Acronym": "WSM"
+ },
+ {
+ "FlagName": "BQ",
+ "FullName": "Bonaire, Saint Eustatius and Saba",
+ "Acronym": "BES"
+ },
+ {
+ "FlagName": "BR",
+ "FullName": "Brazil",
+ "Acronym": "BRA"
+ },
+ {
+ "FlagName": "BS",
+ "FullName": "Bahamas",
+ "Acronym": "BHS"
+ },
+ {
+ "FlagName": "JE",
+ "FullName": "Jersey",
+ "Acronym": "JEY"
+ },
+ {
+ "FlagName": "BY",
+ "FullName": "Belarus",
+ "Acronym": "BLR"
+ },
+ {
+ "FlagName": "BZ",
+ "FullName": "Belize",
+ "Acronym": "BLZ"
+ },
+ {
+ "FlagName": "RU",
+ "FullName": "Russia",
+ "Acronym": "RUS"
+ },
+ {
+ "FlagName": "RW",
+ "FullName": "Rwanda",
+ "Acronym": "RWA"
+ },
+ {
+ "FlagName": "RS",
+ "FullName": "Serbia",
+ "Acronym": "SRB"
+ },
+ {
+ "FlagName": "TL",
+ "FullName": "East Timor",
+ "Acronym": "TLS"
+ },
+ {
+ "FlagName": "RE",
+ "FullName": "Reunion",
+ "Acronym": "REU"
+ },
+ {
+ "FlagName": "TM",
+ "FullName": "Turkmenistan",
+ "Acronym": "TKM"
+ },
+ {
+ "FlagName": "TJ",
+ "FullName": "Tajikistan",
+ "Acronym": "TJK"
+ },
+ {
+ "FlagName": "RO",
+ "FullName": "Romania",
+ "Acronym": "ROU"
+ },
+ {
+ "FlagName": "TK",
+ "FullName": "Tokelau",
+ "Acronym": "TKL"
+ },
+ {
+ "FlagName": "GW",
+ "FullName": "Guinea-Bissau",
+ "Acronym": "GNB"
+ },
+ {
+ "FlagName": "GU",
+ "FullName": "Guam",
+ "Acronym": "GUM"
+ },
+ {
+ "FlagName": "GT",
+ "FullName": "Guatemala",
+ "Acronym": "GTM"
+ },
+ {
+ "FlagName": "GS",
+ "FullName": "South Georgia and the South Sandwich Islands",
+ "Acronym": "SGS"
+ },
+ {
+ "FlagName": "GR",
+ "FullName": "Greece",
+ "Acronym": "GRC"
+ },
+ {
+ "FlagName": "GQ",
+ "FullName": "Equatorial Guinea",
+ "Acronym": "GNQ"
+ },
+ {
+ "FlagName": "GP",
+ "FullName": "Guadeloupe",
+ "Acronym": "GLP"
+ },
+ {
+ "FlagName": "JP",
+ "FullName": "Japan",
+ "Acronym": "JPN"
+ },
+ {
+ "FlagName": "GY",
+ "FullName": "Guyana",
+ "Acronym": "GUY"
+ },
+ {
+ "FlagName": "GG",
+ "FullName": "Guernsey",
+ "Acronym": "GGY"
+ },
+ {
+ "FlagName": "GF",
+ "FullName": "French Guiana",
+ "Acronym": "GUF"
+ },
+ {
+ "FlagName": "GE",
+ "FullName": "Georgia",
+ "Acronym": "GEO"
+ },
+ {
+ "FlagName": "GD",
+ "FullName": "Grenada",
+ "Acronym": "GRD"
+ },
+ {
+ "FlagName": "GB",
+ "FullName": "United Kingdom",
+ "Acronym": "GBR"
+ },
+ {
+ "FlagName": "GA",
+ "FullName": "Gabon",
+ "Acronym": "GAB"
+ },
+ {
+ "FlagName": "SV",
+ "FullName": "El Salvador",
+ "Acronym": "SLV"
+ },
+ {
+ "FlagName": "GN",
+ "FullName": "Guinea",
+ "Acronym": "GIN"
+ },
+ {
+ "FlagName": "GM",
+ "FullName": "Gambia",
+ "Acronym": "GMB"
+ },
+ {
+ "FlagName": "GL",
+ "FullName": "Greenland",
+ "Acronym": "GRL"
+ },
+ {
+ "FlagName": "GI",
+ "FullName": "Gibraltar",
+ "Acronym": "GIB"
+ },
+ {
+ "FlagName": "GH",
+ "FullName": "Ghana",
+ "Acronym": "GHA"
+ },
+ {
+ "FlagName": "OM",
+ "FullName": "Oman",
+ "Acronym": "OMN"
+ },
+ {
+ "FlagName": "TN",
+ "FullName": "Tunisia",
+ "Acronym": "TUN"
+ },
+ {
+ "FlagName": "JO",
+ "FullName": "Jordan",
+ "Acronym": "JOR"
+ },
+ {
+ "FlagName": "HR",
+ "FullName": "Croatia",
+ "Acronym": "HRV"
+ },
+ {
+ "FlagName": "HT",
+ "FullName": "Haiti",
+ "Acronym": "HTI"
+ },
+ {
+ "FlagName": "HU",
+ "FullName": "Hungary",
+ "Acronym": "HUN"
+ },
+ {
+ "FlagName": "HK",
+ "FullName": "Hong Kong",
+ "Acronym": "HKG"
+ },
+ {
+ "FlagName": "HN",
+ "FullName": "Honduras",
+ "Acronym": "HND"
+ },
+ {
+ "FlagName": "HM",
+ "FullName": "Heard Island and McDonald Islands",
+ "Acronym": "HMD"
+ },
+ {
+ "FlagName": "VE",
+ "FullName": "Venezuela",
+ "Acronym": "VEN"
+ },
+ {
+ "FlagName": "PR",
+ "FullName": "Puerto Rico",
+ "Acronym": "PRI"
+ },
+ {
+ "FlagName": "PS",
+ "FullName": "Palestinian Territory",
+ "Acronym": "PSE"
+ },
+ {
+ "FlagName": "PW",
+ "FullName": "Palau",
+ "Acronym": "PLW"
+ },
+ {
+ "FlagName": "PT",
+ "FullName": "Portugal",
+ "Acronym": "PRT"
+ },
+ {
+ "FlagName": "SJ",
+ "FullName": "Svalbard and Jan Mayen",
+ "Acronym": "SJM"
+ },
+ {
+ "FlagName": "PY",
+ "FullName": "Paraguay",
+ "Acronym": "PRY"
+ },
+ {
+ "FlagName": "IQ",
+ "FullName": "Iraq",
+ "Acronym": "IRQ"
+ },
+ {
+ "FlagName": "PA",
+ "FullName": "Panama",
+ "Acronym": "PAN"
+ },
+ {
+ "FlagName": "PF",
+ "FullName": "French Polynesia",
+ "Acronym": "PYF"
+ },
+ {
+ "FlagName": "PG",
+ "FullName": "Papua New Guinea",
+ "Acronym": "PNG"
+ },
+ {
+ "FlagName": "PE",
+ "FullName": "Peru",
+ "Acronym": "PER"
+ },
+ {
+ "FlagName": "PK",
+ "FullName": "Pakistan",
+ "Acronym": "PAK"
+ },
+ {
+ "FlagName": "PH",
+ "FullName": "Philippines",
+ "Acronym": "PHL"
+ },
+ {
+ "FlagName": "PN",
+ "FullName": "Pitcairn",
+ "Acronym": "PCN"
+ },
+ {
+ "FlagName": "PL",
+ "FullName": "Poland",
+ "Acronym": "POL"
+ },
+ {
+ "FlagName": "PM",
+ "FullName": "Saint Pierre and Miquelon",
+ "Acronym": "SPM"
+ },
+ {
+ "FlagName": "ZM",
+ "FullName": "Zambia",
+ "Acronym": "ZMB"
+ },
+ {
+ "FlagName": "EH",
+ "FullName": "Western Sahara",
+ "Acronym": "ESH"
+ },
+ {
+ "FlagName": "EE",
+ "FullName": "Estonia",
+ "Acronym": "EST"
+ },
+ {
+ "FlagName": "EG",
+ "FullName": "Egypt",
+ "Acronym": "EGY"
+ },
+ {
+ "FlagName": "ZA",
+ "FullName": "South Africa",
+ "Acronym": "ZAF"
+ },
+ {
+ "FlagName": "EC",
+ "FullName": "Ecuador",
+ "Acronym": "ECU"
+ },
+ {
+ "FlagName": "IT",
+ "FullName": "Italy",
+ "Acronym": "ITA"
+ },
+ {
+ "FlagName": "VN",
+ "FullName": "Vietnam",
+ "Acronym": "VNM"
+ },
+ {
+ "FlagName": "SB",
+ "FullName": "Solomon Islands",
+ "Acronym": "SLB"
+ },
+ {
+ "FlagName": "ET",
+ "FullName": "Ethiopia",
+ "Acronym": "ETH"
+ },
+ {
+ "FlagName": "SO",
+ "FullName": "Somalia",
+ "Acronym": "SOM"
+ },
+ {
+ "FlagName": "ZW",
+ "FullName": "Zimbabwe",
+ "Acronym": "ZWE"
+ },
+ {
+ "FlagName": "SA",
+ "FullName": "Saudi Arabia",
+ "Acronym": "SAU"
+ },
+ {
+ "FlagName": "ES",
+ "FullName": "Spain",
+ "Acronym": "ESP"
+ },
+ {
+ "FlagName": "ER",
+ "FullName": "Eritrea",
+ "Acronym": "ERI"
+ },
+ {
+ "FlagName": "ME",
+ "FullName": "Montenegro",
+ "Acronym": "MNE"
+ },
+ {
+ "FlagName": "MD",
+ "FullName": "Moldova",
+ "Acronym": "MDA"
+ },
+ {
+ "FlagName": "MG",
+ "FullName": "Madagascar",
+ "Acronym": "MDG"
+ },
+ {
+ "FlagName": "MF",
+ "FullName": "Saint Martin",
+ "Acronym": "MAF"
+ },
+ {
+ "FlagName": "MA",
+ "FullName": "Morocco",
+ "Acronym": "MAR"
+ },
+ {
+ "FlagName": "MC",
+ "FullName": "Monaco",
+ "Acronym": "MCO"
+ },
+ {
+ "FlagName": "UZ",
+ "FullName": "Uzbekistan",
+ "Acronym": "UZB"
+ },
+ {
+ "FlagName": "MM",
+ "FullName": "Myanmar",
+ "Acronym": "MMR"
+ },
+ {
+ "FlagName": "ML",
+ "FullName": "Mali",
+ "Acronym": "MLI"
+ },
+ {
+ "FlagName": "MO",
+ "FullName": "Macao",
+ "Acronym": "MAC"
+ },
+ {
+ "FlagName": "MN",
+ "FullName": "Mongolia",
+ "Acronym": "MNG"
+ },
+ {
+ "FlagName": "MH",
+ "FullName": "Marshall Islands",
+ "Acronym": "MHL"
+ },
+ {
+ "FlagName": "MK",
+ "FullName": "Macedonia",
+ "Acronym": "MKD"
+ },
+ {
+ "FlagName": "MU",
+ "FullName": "Mauritius",
+ "Acronym": "MUS"
+ },
+ {
+ "FlagName": "MT",
+ "FullName": "Malta",
+ "Acronym": "MLT"
+ },
+ {
+ "FlagName": "MW",
+ "FullName": "Malawi",
+ "Acronym": "MWI"
+ },
+ {
+ "FlagName": "MV",
+ "FullName": "Maldives",
+ "Acronym": "MDV"
+ },
+ {
+ "FlagName": "MQ",
+ "FullName": "Martinique",
+ "Acronym": "MTQ"
+ },
+ {
+ "FlagName": "MP",
+ "FullName": "Northern Mariana Islands",
+ "Acronym": "MNP"
+ },
+ {
+ "FlagName": "MS",
+ "FullName": "Montserrat",
+ "Acronym": "MSR"
+ },
+ {
+ "FlagName": "MR",
+ "FullName": "Mauritania",
+ "Acronym": "MRT"
+ },
+ {
+ "FlagName": "IM",
+ "FullName": "Isle of Man",
+ "Acronym": "IMN"
+ },
+ {
+ "FlagName": "UG",
+ "FullName": "Uganda",
+ "Acronym": "UGA"
+ },
+ {
+ "FlagName": "TZ",
+ "FullName": "Tanzania",
+ "Acronym": "TZA"
+ },
+ {
+ "FlagName": "MY",
+ "FullName": "Malaysia",
+ "Acronym": "MYS"
+ },
+ {
+ "FlagName": "MX",
+ "FullName": "Mexico",
+ "Acronym": "MEX"
+ },
+ {
+ "FlagName": "IL",
+ "FullName": "Israel",
+ "Acronym": "ISR"
+ },
+ {
+ "FlagName": "FR",
+ "FullName": "France",
+ "Acronym": "FRA"
+ },
+ {
+ "FlagName": "IO",
+ "FullName": "British Indian Ocean Territory",
+ "Acronym": "IOT"
+ },
+ {
+ "FlagName": "SH",
+ "FullName": "Saint Helena",
+ "Acronym": "SHN"
+ },
+ {
+ "FlagName": "FI",
+ "FullName": "Finland",
+ "Acronym": "FIN"
+ },
+ {
+ "FlagName": "FJ",
+ "FullName": "Fiji",
+ "Acronym": "FJI"
+ },
+ {
+ "FlagName": "FK",
+ "FullName": "Falkland Islands",
+ "Acronym": "FLK"
+ },
+ {
+ "FlagName": "FM",
+ "FullName": "Micronesia",
+ "Acronym": "FSM"
+ },
+ {
+ "FlagName": "FO",
+ "FullName": "Faroe Islands",
+ "Acronym": "FRO"
+ },
+ {
+ "FlagName": "NI",
+ "FullName": "Nicaragua",
+ "Acronym": "NIC"
+ },
+ {
+ "FlagName": "NL",
+ "FullName": "Netherlands",
+ "Acronym": "NLD"
+ },
+ {
+ "FlagName": "NO",
+ "FullName": "Norway",
+ "Acronym": "NOR"
+ },
+ {
+ "FlagName": "NA",
+ "FullName": "Namibia",
+ "Acronym": "NAM"
+ },
+ {
+ "FlagName": "VU",
+ "FullName": "Vanuatu",
+ "Acronym": "VUT"
+ },
+ {
+ "FlagName": "NC",
+ "FullName": "New Caledonia",
+ "Acronym": "NCL"
+ },
+ {
+ "FlagName": "NE",
+ "FullName": "Niger",
+ "Acronym": "NER"
+ },
+ {
+ "FlagName": "NF",
+ "FullName": "Norfolk Island",
+ "Acronym": "NFK"
+ },
+ {
+ "FlagName": "NG",
+ "FullName": "Nigeria",
+ "Acronym": "NGA"
+ },
+ {
+ "FlagName": "NZ",
+ "FullName": "New Zealand",
+ "Acronym": "NZL"
+ },
+ {
+ "FlagName": "NP",
+ "FullName": "Nepal",
+ "Acronym": "NPL"
+ },
+ {
+ "FlagName": "NR",
+ "FullName": "Nauru",
+ "Acronym": "NRU"
+ },
+ {
+ "FlagName": "NU",
+ "FullName": "Niue",
+ "Acronym": "NIU"
+ },
+ {
+ "FlagName": "CK",
+ "FullName": "Cook Islands",
+ "Acronym": "COK"
+ },
+ {
+ "FlagName": "XK",
+ "FullName": "Kosovo",
+ "Acronym": "XKX"
+ },
+ {
+ "FlagName": "CI",
+ "FullName": "Ivory Coast",
+ "Acronym": "CIV"
+ },
+ {
+ "FlagName": "CH",
+ "FullName": "Switzerland",
+ "Acronym": "CHE"
+ },
+ {
+ "FlagName": "CO",
+ "FullName": "Colombia",
+ "Acronym": "COL"
+ },
+ {
+ "FlagName": "CN",
+ "FullName": "China",
+ "Acronym": "CHN"
+ },
+ {
+ "FlagName": "CM",
+ "FullName": "Cameroon",
+ "Acronym": "CMR"
+ },
+ {
+ "FlagName": "CL",
+ "FullName": "Chile",
+ "Acronym": "CHL"
+ },
+ {
+ "FlagName": "CC",
+ "FullName": "Cocos Islands",
+ "Acronym": "CCK"
+ },
+ {
+ "FlagName": "CA",
+ "FullName": "Canada",
+ "Acronym": "CAN"
+ },
+ {
+ "FlagName": "CG",
+ "FullName": "Republic of the Congo",
+ "Acronym": "COG"
+ },
+ {
+ "FlagName": "CF",
+ "FullName": "Central African Republic",
+ "Acronym": "CAF"
+ },
+ {
+ "FlagName": "CD",
+ "FullName": "Democratic Republic of the Congo",
+ "Acronym": "COD"
+ },
+ {
+ "FlagName": "CZ",
+ "FullName": "Czech Republic",
+ "Acronym": "CZE"
+ },
+ {
+ "FlagName": "CY",
+ "FullName": "Cyprus",
+ "Acronym": "CYP"
+ },
+ {
+ "FlagName": "CX",
+ "FullName": "Christmas Island",
+ "Acronym": "CXR"
+ },
+ {
+ "FlagName": "CR",
+ "FullName": "Costa Rica",
+ "Acronym": "CRI"
+ },
+ {
+ "FlagName": "CW",
+ "FullName": "Curacao",
+ "Acronym": "CUW"
+ },
+ {
+ "FlagName": "CV",
+ "FullName": "Cape Verde",
+ "Acronym": "CPV"
+ },
+ {
+ "FlagName": "CU",
+ "FullName": "Cuba",
+ "Acronym": "CUB"
+ },
+ {
+ "FlagName": "SZ",
+ "FullName": "Swaziland",
+ "Acronym": "SWZ"
+ },
+ {
+ "FlagName": "SY",
+ "FullName": "Syria",
+ "Acronym": "SYR"
+ },
+ {
+ "FlagName": "SX",
+ "FullName": "Sint Maarten",
+ "Acronym": "SXM"
+ },
+ {
+ "FlagName": "KG",
+ "FullName": "Kyrgyzstan",
+ "Acronym": "KGZ"
+ },
+ {
+ "FlagName": "KE",
+ "FullName": "Kenya",
+ "Acronym": "KEN"
+ },
+ {
+ "FlagName": "SS",
+ "FullName": "South Sudan",
+ "Acronym": "SSD"
+ },
+ {
+ "FlagName": "SR",
+ "FullName": "Suriname",
+ "Acronym": "SUR"
+ },
+ {
+ "FlagName": "KI",
+ "FullName": "Kiribati",
+ "Acronym": "KIR"
+ },
+ {
+ "FlagName": "KH",
+ "FullName": "Cambodia",
+ "Acronym": "KHM"
+ },
+ {
+ "FlagName": "KN",
+ "FullName": "Saint Kitts and Nevis",
+ "Acronym": "KNA"
+ },
+ {
+ "FlagName": "KM",
+ "FullName": "Comoros",
+ "Acronym": "COM"
+ },
+ {
+ "FlagName": "ST",
+ "FullName": "Sao Tome and Principe",
+ "Acronym": "STP"
+ },
+ {
+ "FlagName": "SK",
+ "FullName": "Slovakia",
+ "Acronym": "SVK"
+ },
+ {
+ "FlagName": "KR",
+ "FullName": "South Korea",
+ "Acronym": "KOR"
+ },
+ {
+ "FlagName": "SI",
+ "FullName": "Slovenia",
+ "Acronym": "SVN"
+ },
+ {
+ "FlagName": "KP",
+ "FullName": "North Korea",
+ "Acronym": "PRK"
+ },
+ {
+ "FlagName": "KW",
+ "FullName": "Kuwait",
+ "Acronym": "KWT"
+ },
+ {
+ "FlagName": "SN",
+ "FullName": "Senegal",
+ "Acronym": "SEN"
+ },
+ {
+ "FlagName": "SM",
+ "FullName": "San Marino",
+ "Acronym": "SMR"
+ },
+ {
+ "FlagName": "SL",
+ "FullName": "Sierra Leone",
+ "Acronym": "SLE"
+ },
+ {
+ "FlagName": "SC",
+ "FullName": "Seychelles",
+ "Acronym": "SYC"
+ },
+ {
+ "FlagName": "KZ",
+ "FullName": "Kazakhstan",
+ "Acronym": "KAZ"
+ },
+ {
+ "FlagName": "KY",
+ "FullName": "Cayman Islands",
+ "Acronym": "CYM"
+ },
+ {
+ "FlagName": "SG",
+ "FullName": "Singapore",
+ "Acronym": "SGP"
+ },
+ {
+ "FlagName": "SE",
+ "FullName": "Sweden",
+ "Acronym": "SWE"
+ },
+ {
+ "FlagName": "SD",
+ "FullName": "Sudan",
+ "Acronym": "SDN"
+ },
+ {
+ "FlagName": "DO",
+ "FullName": "Dominican Republic",
+ "Acronym": "DOM"
+ },
+ {
+ "FlagName": "DM",
+ "FullName": "Dominica",
+ "Acronym": "DMA"
+ },
+ {
+ "FlagName": "DJ",
+ "FullName": "Djibouti",
+ "Acronym": "DJI"
+ },
+ {
+ "FlagName": "DK",
+ "FullName": "Denmark",
+ "Acronym": "DNK"
+ },
+ {
+ "FlagName": "VG",
+ "FullName": "British Virgin Islands",
+ "Acronym": "VGB"
+ },
+ {
+ "FlagName": "DE",
+ "FullName": "Germany",
+ "Acronym": "DEU"
+ },
+ {
+ "FlagName": "YE",
+ "FullName": "Yemen",
+ "Acronym": "YEM"
+ },
+ {
+ "FlagName": "DZ",
+ "FullName": "Algeria",
+ "Acronym": "DZA"
+ },
+ {
+ "FlagName": "US",
+ "FullName": "United States",
+ "Acronym": "USA"
+ },
+ {
+ "FlagName": "UY",
+ "FullName": "Uruguay",
+ "Acronym": "URY"
+ },
+ {
+ "FlagName": "YT",
+ "FullName": "Mayotte",
+ "Acronym": "MYT"
+ },
+ {
+ "FlagName": "UM",
+ "FullName": "United States Minor Outlying Islands",
+ "Acronym": "UMI"
+ },
+ {
+ "FlagName": "LB",
+ "FullName": "Lebanon",
+ "Acronym": "LBN"
+ },
+ {
+ "FlagName": "LC",
+ "FullName": "Saint Lucia",
+ "Acronym": "LCA"
+ },
+ {
+ "FlagName": "LA",
+ "FullName": "Laos",
+ "Acronym": "LAO"
+ },
+ {
+ "FlagName": "TV",
+ "FullName": "Tuvalu",
+ "Acronym": "TUV"
+ },
+ {
+ "FlagName": "TW",
+ "FullName": "Taiwan",
+ "Acronym": "TWN"
+ },
+ {
+ "FlagName": "TT",
+ "FullName": "Trinidad and Tobago",
+ "Acronym": "TTO"
+ },
+ {
+ "FlagName": "TR",
+ "FullName": "Turkey",
+ "Acronym": "TUR"
+ },
+ {
+ "FlagName": "LK",
+ "FullName": "Sri Lanka",
+ "Acronym": "LKA"
+ },
+ {
+ "FlagName": "LI",
+ "FullName": "Liechtenstein",
+ "Acronym": "LIE"
+ },
+ {
+ "FlagName": "LV",
+ "FullName": "Latvia",
+ "Acronym": "LVA"
+ },
+ {
+ "FlagName": "TO",
+ "FullName": "Tonga",
+ "Acronym": "TON"
+ },
+ {
+ "FlagName": "LT",
+ "FullName": "Lithuania",
+ "Acronym": "LTU"
+ },
+ {
+ "FlagName": "LU",
+ "FullName": "Luxembourg",
+ "Acronym": "LUX"
+ },
+ {
+ "FlagName": "LR",
+ "FullName": "Liberia",
+ "Acronym": "LBR"
+ },
+ {
+ "FlagName": "LS",
+ "FullName": "Lesotho",
+ "Acronym": "LSO"
+ },
+ {
+ "FlagName": "TH",
+ "FullName": "Thailand",
+ "Acronym": "THA"
+ },
+ {
+ "FlagName": "TF",
+ "FullName": "French Southern Territories",
+ "Acronym": "ATF"
+ },
+ {
+ "FlagName": "TG",
+ "FullName": "Togo",
+ "Acronym": "TGO"
+ },
+ {
+ "FlagName": "TD",
+ "FullName": "Chad",
+ "Acronym": "TCD"
+ },
+ {
+ "FlagName": "TC",
+ "FullName": "Turks and Caicos Islands",
+ "Acronym": "TCA"
+ },
+ {
+ "FlagName": "LY",
+ "FullName": "Libya",
+ "Acronym": "LBY"
+ },
+ {
+ "FlagName": "VA",
+ "FullName": "Vatican",
+ "Acronym": "VAT"
+ },
+ {
+ "FlagName": "VC",
+ "FullName": "Saint Vincent and the Grenadines",
+ "Acronym": "VCT"
+ },
+ {
+ "FlagName": "AE",
+ "FullName": "United Arab Emirates",
+ "Acronym": "ARE"
+ },
+ {
+ "FlagName": "AD",
+ "FullName": "Andorra",
+ "Acronym": "AND"
+ },
+ {
+ "FlagName": "AG",
+ "FullName": "Antigua and Barbuda",
+ "Acronym": "ATG"
+ },
+ {
+ "FlagName": "AF",
+ "FullName": "Afghanistan",
+ "Acronym": "AFG"
+ },
+ {
+ "FlagName": "AI",
+ "FullName": "Anguilla",
+ "Acronym": "AIA"
+ },
+ {
+ "FlagName": "VI",
+ "FullName": "U.S. Virgin Islands",
+ "Acronym": "VIR"
+ },
+ {
+ "FlagName": "IS",
+ "FullName": "Iceland",
+ "Acronym": "ISL"
+ },
+ {
+ "FlagName": "IR",
+ "FullName": "Iran",
+ "Acronym": "IRN"
+ },
+ {
+ "FlagName": "AM",
+ "FullName": "Armenia",
+ "Acronym": "ARM"
+ },
+ {
+ "FlagName": "AL",
+ "FullName": "Albania",
+ "Acronym": "ALB"
+ },
+ {
+ "FlagName": "AO",
+ "FullName": "Angola",
+ "Acronym": "AGO"
+ },
+ {
+ "FlagName": "AQ",
+ "FullName": "Antarctica",
+ "Acronym": "ATA"
+ },
+ {
+ "FlagName": "AS",
+ "FullName": "American Samoa",
+ "Acronym": "ASM"
+ },
+ {
+ "FlagName": "AR",
+ "FullName": "Argentina",
+ "Acronym": "ARG"
+ },
+ {
+ "FlagName": "AU",
+ "FullName": "Australia",
+ "Acronym": "AUS"
+ },
+ {
+ "FlagName": "AT",
+ "FullName": "Austria",
+ "Acronym": "AUT"
+ },
+ {
+ "FlagName": "AW",
+ "FullName": "Aruba",
+ "Acronym": "ABW"
+ },
+ {
+ "FlagName": "IN",
+ "FullName": "India",
+ "Acronym": "IND"
+ },
+ {
+ "FlagName": "AX",
+ "FullName": "Aland Islands",
+ "Acronym": "ALA"
+ },
+ {
+ "FlagName": "AZ",
+ "FullName": "Azerbaijan",
+ "Acronym": "AZE"
+ },
+ {
+ "FlagName": "IE",
+ "FullName": "Ireland",
+ "Acronym": "IRL"
+ },
+ {
+ "FlagName": "ID",
+ "FullName": "Indonesia",
+ "Acronym": "IDN"
+ },
+ {
+ "FlagName": "UA",
+ "FullName": "Ukraine",
+ "Acronym": "UKR"
+ },
+ {
+ "FlagName": "QA",
+ "FullName": "Qatar",
+ "Acronym": "QAT"
+ },
+ {
+ "FlagName": "MZ",
+ "FullName": "Mozambique",
+ "Acronym": "MOZ"
+ }
+]
\ No newline at end of file
diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs
new file mode 100644
index 0000000000..fccd35ca9e
--- /dev/null
+++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Legacy;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.IPC;
+
+namespace osu.Game.Tournament.Screens
+{
+ public abstract class BeatmapInfoScreen : TournamentScreen
+ {
+ protected readonly SongBar SongBar;
+
+ protected BeatmapInfoScreen()
+ {
+ AddInternal(SongBar = new SongBar
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(MatchIPCInfo ipc)
+ {
+ ipc.Beatmap.BindValueChanged(beatmapChanged, true);
+ ipc.Mods.BindValueChanged(modsChanged, true);
+ }
+
+ private void modsChanged(ValueChangedEvent mods)
+ {
+ SongBar.Mods = mods.NewValue;
+ }
+
+ private void beatmapChanged(ValueChangedEvent beatmap)
+ {
+ SongBar.FadeInFromZero(300, Easing.OutQuint);
+ SongBar.Beatmap = beatmap.NewValue;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Tournament/Components/DrawingsConfigManager.cs b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
similarity index 92%
rename from osu.Game/Screens/Tournament/Components/DrawingsConfigManager.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
index 36af1389ff..d197c0f5d9 100644
--- a/osu.Game/Screens/Tournament/Components/DrawingsConfigManager.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
@@ -4,7 +4,7 @@
using osu.Framework.Configuration;
using osu.Framework.Platform;
-namespace osu.Game.Screens.Tournament.Components
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class DrawingsConfigManager : IniConfigManager
{
diff --git a/osu.Game/Screens/Tournament/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs
similarity index 70%
rename from osu.Game/Screens/Tournament/Group.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/Group.cs
index 4fed10b94d..549ff26018 100644
--- a/osu.Game/Screens/Tournament/Group.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs
@@ -4,19 +4,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Graphics.Sprites;
-using osuTK;
-using osuTK.Graphics;
-using osu.Game.Screens.Tournament.Teams;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osuTK;
+using osuTK.Graphics;
-namespace osu.Game.Screens.Tournament
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class Group : Container
{
@@ -73,7 +71,7 @@ namespace osu.Game.Screens.Tournament
};
}
- public void AddTeam(DrawingsTeam team)
+ public void AddTeam(TournamentTeam team)
{
GroupTeam gt = new GroupTeam(team);
@@ -88,10 +86,10 @@ namespace osu.Game.Screens.Tournament
public bool ContainsTeam(string fullName)
{
- return allTeams.Any(t => t.Team.FullName == fullName);
+ return allTeams.Any(t => t.Team.FullName.Value == fullName);
}
- public bool RemoveTeam(DrawingsTeam team)
+ public bool RemoveTeam(TournamentTeam team)
{
allTeams.RemoveAll(gt => gt.Team == team);
@@ -116,25 +114,29 @@ namespace osu.Game.Screens.Tournament
{
StringBuilder sb = new StringBuilder();
foreach (GroupTeam gt in allTeams)
- sb.AppendLine(gt.Team.FullName);
+ sb.AppendLine(gt.Team.FullName.Value);
return sb.ToString();
}
- private class GroupTeam : Container
+ private class GroupTeam : DrawableTournamentTeam
{
- public readonly DrawingsTeam Team;
-
private readonly FillFlowContainer innerContainer;
- private readonly Sprite flagSprite;
- public GroupTeam(DrawingsTeam team)
+ public GroupTeam(TournamentTeam team)
+ : base(team)
{
- Team = team;
-
Width = 36;
AutoSizeAxes = Axes.Y;
- Children = new Drawable[]
+ Flag.Anchor = Anchor.TopCentre;
+ Flag.Origin = Anchor.TopCentre;
+
+ AcronymText.Anchor = Anchor.TopCentre;
+ AcronymText.Origin = Anchor.TopCentre;
+ AcronymText.Text = team.Acronym.Value.ToUpperInvariant();
+ AcronymText.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 10);
+
+ InternalChildren = new Drawable[]
{
innerContainer = new FillFlowContainer
{
@@ -149,21 +151,8 @@ namespace osu.Game.Screens.Tournament
Children = new Drawable[]
{
- flagSprite = new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- FillMode = FillMode.Fit
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
-
- Text = team.Acronym.ToUpperInvariant(),
- Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 10),
- }
+ Flag,
+ AcronymText
}
}
};
@@ -175,12 +164,6 @@ namespace osu.Game.Screens.Tournament
innerContainer.ScaleTo(1.5f);
innerContainer.ScaleTo(1f, 200);
}
-
- [BackgroundDependencyLoader]
- private void load(TextureStore textures)
- {
- flagSprite.Texture = textures.Get($@"Flags/{Team.FlagName}");
- }
}
}
}
diff --git a/osu.Game/Screens/Tournament/GroupContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs
similarity index 95%
rename from osu.Game/Screens/Tournament/GroupContainer.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs
index 211a28ca38..8a66ca7bf6 100644
--- a/osu.Game/Screens/Tournament/GroupContainer.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupContainer.cs
@@ -7,10 +7,10 @@ using System.Linq;
using System.Text;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Tournament.Models;
using osuTK;
-using osu.Game.Screens.Tournament.Teams;
-namespace osu.Game.Screens.Tournament
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class GroupContainer : Container
{
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.Tournament
}
}
- public void AddTeam(DrawingsTeam team)
+ public void AddTeam(TournamentTeam team)
{
if (groups[currentGroup].TeamsCount == maxTeams)
return;
diff --git a/osu.Game/Screens/Tournament/Teams/ITeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs
similarity index 62%
rename from osu.Game/Screens/Tournament/Teams/ITeamList.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs
index e998c5dce4..09208818a9 100644
--- a/osu.Game/Screens/Tournament/Teams/ITeamList.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/ITeamList.cs
@@ -2,11 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Game.Tournament.Models;
-namespace osu.Game.Screens.Tournament.Teams
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public interface ITeamList
{
- IEnumerable Teams { get; }
+ IEnumerable Teams { get; }
}
}
diff --git a/osu.Game/Screens/Tournament/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
similarity index 88%
rename from osu.Game/Screens/Tournament/ScrollingTeamContainer.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
index 02f7f73399..b147d680f0 100644
--- a/osu.Game/Screens/Tournament/ScrollingTeamContainer.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
@@ -5,26 +5,25 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Threading;
-using osu.Game.Screens.Tournament.Teams;
+using osu.Game.Graphics;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Screens.Tournament
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class ScrollingTeamContainer : Container
{
public event Action OnScrollStarted;
- public event Action OnSelected;
+ public event Action OnSelected;
- private readonly List availableTeams = new List();
+ private readonly List availableTeams = new List();
private readonly Container tracker;
@@ -55,6 +54,7 @@ namespace osu.Game.Screens.Tournament
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.33f),
Masking = true,
CornerRadius = 10f,
@@ -170,7 +170,7 @@ namespace osu.Game.Screens.Tournament
}
}
- public void AddTeam(DrawingsTeam team)
+ public void AddTeam(TournamentTeam team)
{
if (availableTeams.Contains(team))
return;
@@ -181,12 +181,12 @@ namespace osu.Game.Screens.Tournament
scrollState = ScrollState.Idle;
}
- public void AddTeams(IEnumerable teams)
+ public void AddTeams(IEnumerable teams)
{
if (teams == null)
return;
- foreach (DrawingsTeam t in teams)
+ foreach (TournamentTeam t in teams)
AddTeam(t);
}
@@ -197,7 +197,7 @@ namespace osu.Game.Screens.Tournament
scrollState = ScrollState.Idle;
}
- public void RemoveTeam(DrawingsTeam team)
+ public void RemoveTeam(TournamentTeam team)
{
availableTeams.Remove(team);
@@ -282,7 +282,7 @@ namespace osu.Game.Screens.Tournament
private void addFlags()
{
- foreach (DrawingsTeam t in availableTeams)
+ foreach (TournamentTeam t in availableTeams)
{
Add(new ScrollingTeam(t)
{
@@ -319,14 +319,11 @@ namespace osu.Game.Screens.Tournament
Scrolling
}
- public class ScrollingTeam : Container
+ public class ScrollingTeam : DrawableTournamentTeam
{
public const float WIDTH = 58;
public const float HEIGHT = 41;
- public DrawingsTeam Team;
-
- private readonly Sprite flagSprite;
private readonly Box outline;
private bool selected;
@@ -346,10 +343,9 @@ namespace osu.Game.Screens.Tournament
}
}
- public ScrollingTeam(DrawingsTeam team)
+ public ScrollingTeam(TournamentTeam team)
+ : base(team)
{
- Team = team;
-
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
@@ -359,28 +355,21 @@ namespace osu.Game.Screens.Tournament
Alpha = 0;
- Children = new Drawable[]
+ Flag.Anchor = Anchor.Centre;
+ Flag.Origin = Anchor.Centre;
+ Flag.Scale = new Vector2(0.9f);
+
+ InternalChildren = new Drawable[]
{
outline = new Box
{
RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.33f),
Alpha = 0
},
- flagSprite = new Sprite
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
-
- Size = new Vector2(WIDTH, HEIGHT) - new Vector2(8)
- }
+ Flag
};
}
-
- [BackgroundDependencyLoader]
- private void load(TextureStore textures)
- {
- flagSprite.Texture = textures.Get($@"Flags/{Team.FlagName}");
- }
}
}
}
diff --git a/osu.Game/Screens/Tournament/Teams/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
similarity index 73%
rename from osu.Game/Screens/Tournament/Teams/StorageBackedTeamList.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
index fad47c0922..f96ec01cbb 100644
--- a/osu.Game/Screens/Tournament/Teams/StorageBackedTeamList.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs
@@ -6,8 +6,9 @@ using System.Collections.Generic;
using System.IO;
using osu.Framework.Logging;
using osu.Framework.Platform;
+using osu.Game.Tournament.Models;
-namespace osu.Game.Screens.Tournament.Teams
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class StorageBackedTeamList : ITeamList
{
@@ -20,11 +21,11 @@ namespace osu.Game.Screens.Tournament.Teams
this.storage = storage;
}
- public IEnumerable Teams
+ public IEnumerable Teams
{
get
{
- var teams = new List();
+ var teams = new List();
try
{
@@ -47,17 +48,11 @@ namespace osu.Game.Screens.Tournament.Teams
continue;
}
- string flagName = split[0].Trim();
- string teamName = split[1].Trim();
-
- string acronym = split.Length >= 3 ? split[2].Trim() : teamName;
- acronym = acronym.Substring(0, Math.Min(3, acronym.Length));
-
- teams.Add(new DrawingsTeam
+ teams.Add(new TournamentTeam
{
- FlagName = flagName,
- FullName = teamName,
- Acronym = acronym
+ FullName = { Value = split[1].Trim(), },
+ Acronym = { Value = split.Length >= 3 ? split[2].Trim() : null, },
+ FlagName = { Value = split[0].Trim() }
});
}
}
diff --git a/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs
similarity index 98%
rename from osu.Game/Screens/Tournament/Components/VisualiserContainer.cs
rename to osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs
index 6b0888313b..1cd942b987 100644
--- a/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs
@@ -1,16 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.MathUtils;
-using System.Collections.Generic;
-using System.Linq;
-namespace osu.Game.Screens.Tournament.Components
+namespace osu.Game.Tournament.Screens.Drawings.Components
{
public class VisualiserContainer : Container
{
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
new file mode 100644
index 0000000000..3a14b6d9c2
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -0,0 +1,265 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Drawings.Components;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens.Drawings
+{
+ public class DrawingsScreen : CompositeDrawable
+ {
+ private const string results_filename = "drawings_results.txt";
+
+ private ScrollingTeamContainer teamsContainer;
+ private GroupContainer groupsContainer;
+ private OsuSpriteText fullTeamNameText;
+
+ private readonly List allTeams = new List();
+
+ private DrawingsConfigManager drawingsConfig;
+
+ private Task writeOp;
+
+ private Storage storage;
+
+ public ITeamList TeamList;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures, Storage storage)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ this.storage = storage;
+
+ if (TeamList == null)
+ TeamList = new StorageBackedTeamList(storage);
+
+ if (!TeamList.Teams.Any())
+ {
+ return;
+ }
+
+ drawingsConfig = new DrawingsConfigManager(storage);
+
+ InternalChildren = new Drawable[]
+ {
+ // Main container
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill,
+ Texture = textures.Get(@"Backgrounds/Drawings/background.png")
+ },
+ // Visualiser
+ new VisualiserContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+
+ RelativeSizeAxes = Axes.X,
+ Size = new Vector2(1, 10),
+
+ Colour = new Color4(255, 204, 34, 255),
+
+ Lines = 6
+ },
+ // Groups
+ groupsContainer = new GroupContainer(drawingsConfig.Get(DrawingsConfig.Groups), drawingsConfig.Get(DrawingsConfig.TeamsPerGroup))
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+
+ Padding = new MarginPadding
+ {
+ Top = 35f,
+ Bottom = 35f
+ }
+ },
+ // Scrolling teams
+ teamsContainer = new ScrollingTeamContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+
+ RelativeSizeAxes = Axes.X,
+ },
+ // Scrolling team name
+ fullTeamNameText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+
+ Position = new Vector2(0, 45f),
+
+ Colour = OsuColour.Gray(0.33f),
+
+ Alpha = 0,
+
+ Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42),
+ }
+ }
+ },
+ // Control panel container
+ new ControlPanel
+ {
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+
+ Text = "Begin random",
+ Action = teamsContainer.StartScrolling,
+ },
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+
+ Text = "Stop random",
+ Action = teamsContainer.StopScrolling,
+ },
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+
+ Text = "Reload",
+ Action = reloadTeams
+ },
+ new ControlPanel.Spacer(),
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+
+ Text = "Reset",
+ Action = () => reset()
+ }
+ }
+ };
+
+ teamsContainer.OnSelected += onTeamSelected;
+ teamsContainer.OnScrollStarted += () => fullTeamNameText.FadeOut(200);
+
+ reset(true);
+ }
+
+ private void onTeamSelected(TournamentTeam team)
+ {
+ groupsContainer.AddTeam(team);
+
+ fullTeamNameText.Text = team.FullName.Value;
+ fullTeamNameText.FadeIn(200);
+
+ writeResults(groupsContainer.GetStringRepresentation());
+ }
+
+ private void writeResults(string text)
+ {
+ void writeAction()
+ {
+ try
+ {
+ // Write to drawings_results
+ using (Stream stream = storage.GetStream(results_filename, FileAccess.Write, FileMode.Create))
+ using (StreamWriter sw = new StreamWriter(stream))
+ {
+ sw.Write(text);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Failed to write results.");
+ }
+ }
+
+ writeOp = writeOp?.ContinueWith(t => { writeAction(); }) ?? Task.Run((Action)writeAction);
+ }
+
+ private void reloadTeams()
+ {
+ teamsContainer.ClearTeams();
+ allTeams.Clear();
+
+ foreach (TournamentTeam t in TeamList.Teams)
+ {
+ if (groupsContainer.ContainsTeam(t.FullName.Value))
+ continue;
+
+ allTeams.Add(t);
+ teamsContainer.AddTeam(t);
+ }
+ }
+
+ private void reset(bool loadLastResults = false)
+ {
+ groupsContainer.ClearTeams();
+
+ reloadTeams();
+
+ if (!storage.Exists(results_filename))
+ return;
+
+ if (loadLastResults)
+ {
+ try
+ {
+ // Read from drawings_results
+ using (Stream stream = storage.GetStream(results_filename, FileAccess.Read, FileMode.Open))
+ using (StreamReader sr = new StreamReader(stream))
+ {
+ string line;
+
+ while ((line = sr.ReadLine()?.Trim()) != null)
+ {
+ if (string.IsNullOrEmpty(line))
+ continue;
+
+ if (line.ToUpperInvariant().StartsWith("GROUP"))
+ continue;
+
+ // ReSharper disable once AccessToModifiedClosure
+ TournamentTeam teamToAdd = allTeams.FirstOrDefault(t => t.FullName.Value == line);
+
+ if (teamToAdd == null)
+ continue;
+
+ groupsContainer.AddTeam(teamToAdd);
+ teamsContainer.RemoveTeam(teamToAdd);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Failed to read last drawings results.");
+ }
+ }
+ else
+ {
+ writeResults(string.Empty);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/IModelBacked.cs b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs
new file mode 100644
index 0000000000..ed94bd7f45
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Editors/IModelBacked.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Tournament.Screens.Editors
+{
+ ///
+ /// Provides a mechanism to access a related model from a representing class.
+ ///
+ /// The type of model.
+ public interface IModelBacked
+ {
+ ///
+ /// The model.
+ ///
+ TModel Model { get; }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
new file mode 100644
index 0000000000..ba63013886
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
@@ -0,0 +1,152 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Framework.Input.States;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Ladder;
+using osu.Game.Tournament.Screens.Ladder.Components;
+using osuTK;
+using osuTK.Graphics;
+using SixLabors.Primitives;
+
+namespace osu.Game.Tournament.Screens.Editors
+{
+ [Cached]
+ public class LadderEditorScreen : LadderScreen, IHasContextMenu
+ {
+ [Cached]
+ private LadderEditorInfo editorInfo = new LadderEditorInfo();
+
+ protected override bool DrawLoserPaths => true;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Content.Add(new LadderEditorSettings
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Margin = new MarginPadding(5)
+ });
+ }
+
+ public void BeginJoin(TournamentMatch match, bool losers)
+ {
+ ScrollContent.Add(new JoinVisualiser(MatchesContainer, match, losers, UpdateLayout));
+ }
+
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ if (editorInfo == null)
+ return new MenuItem[0];
+
+ return new MenuItem[]
+ {
+ new OsuMenuItem("Create new match", MenuItemType.Highlighted, () =>
+ {
+ var pos = MatchesContainer.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position);
+ LadderInfo.Matches.Add(new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } });
+ }),
+ new OsuMenuItem("Reset teams", MenuItemType.Destructive, () =>
+ {
+ foreach (var p in MatchesContainer)
+ p.Match.Reset();
+ })
+ };
+ }
+ }
+
+ public void Remove(TournamentMatch match)
+ {
+ MatchesContainer.FirstOrDefault(p => p.Match == match)?.Remove();
+ }
+
+ private class JoinVisualiser : CompositeDrawable
+ {
+ private readonly Container matchesContainer;
+ public readonly TournamentMatch Source;
+ private readonly bool losers;
+ private readonly Action complete;
+
+ private ProgressionPath path;
+
+ public JoinVisualiser(Container matchesContainer, TournamentMatch source, bool losers, Action complete)
+ {
+ this.matchesContainer = matchesContainer;
+ RelativeSizeAxes = Axes.Both;
+
+ Source = source;
+ this.losers = losers;
+ this.complete = complete;
+ if (losers)
+ Source.LosersProgression.Value = null;
+ else
+ Source.Progression.Value = null;
+ }
+
+ private DrawableTournamentMatch findTarget(InputState state)
+ {
+ return matchesContainer.FirstOrDefault(d => d.ReceivePositionalInputAt(state.Mouse.Position));
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ return true;
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ var found = findTarget(e.CurrentState);
+
+ if (found == path?.Destination)
+ return false;
+
+ path?.Expire();
+ path = null;
+
+ if (found == null)
+ return false;
+
+ AddInternal(path = new ProgressionPath(matchesContainer.First(c => c.Match == Source), found)
+ {
+ Colour = Color4.Yellow,
+ });
+
+ return base.OnMouseMove(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ var found = findTarget(e.CurrentState);
+
+ if (found != null)
+ {
+ if (found.Match != Source)
+ {
+ if (losers)
+ Source.LosersProgression.Value = found.Match;
+ else
+ Source.Progression.Value = found.Match;
+ }
+
+ complete?.Invoke();
+ Expire();
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
new file mode 100644
index 0000000000..1dc91abe6d
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -0,0 +1,282 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osuTK;
+
+namespace osu.Game.Tournament.Screens.Editors
+{
+ public class RoundEditorScreen : TournamentEditorScreen
+ {
+ protected override BindableList Storage => LadderInfo.Rounds;
+
+ public class RoundRow : CompositeDrawable, IModelBacked
+ {
+ public TournamentRound Model { get; }
+
+ [Resolved]
+ private LadderInfo ladderInfo { get; set; }
+
+ public RoundRow(TournamentRound round)
+ {
+ Model = round;
+
+ Masking = true;
+ CornerRadius = 10;
+
+ RoundBeatmapEditor beatmapEditor = new RoundBeatmapEditor(round)
+ {
+ Width = 0.95f
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ Padding = new MarginPadding { Right = 160 },
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new SettingsTextBox
+ {
+ LabelText = "Name",
+ Width = 0.33f,
+ Bindable = Model.Name
+ },
+ new SettingsTextBox
+ {
+ LabelText = "Description",
+ Width = 0.33f,
+ Bindable = Model.Description
+ },
+ new DateTextBox
+ {
+ LabelText = "Start Time",
+ Width = 0.33f,
+ Bindable = Model.StartDate
+ },
+ new SettingsSlider
+ {
+ LabelText = "Best of",
+ Width = 0.33f,
+ Bindable = Model.BestOf
+ },
+ new SettingsButton
+ {
+ Width = 0.2f,
+ Margin = new MarginPadding(10),
+ Text = "Add beatmap",
+ Action = () => beatmapEditor.CreateNew()
+ },
+ beatmapEditor
+ }
+ },
+ new DangerousSettingsButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.None,
+ Width = 150,
+ Text = "Delete Round",
+ Action = () =>
+ {
+ Expire();
+ ladderInfo.Rounds.Remove(Model);
+ },
+ }
+ };
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ public class RoundBeatmapEditor : CompositeDrawable
+ {
+ private readonly TournamentRound round;
+ private readonly FillFlowContainer flow;
+
+ public RoundBeatmapEditor(TournamentRound round)
+ {
+ this.round = round;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = flow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ LayoutDuration = 200,
+ LayoutEasing = Easing.OutQuint,
+ ChildrenEnumerable = round.Beatmaps.Select(p => new RoundBeatmapRow(round, p))
+ };
+ }
+
+ public void CreateNew()
+ {
+ var user = new RoundBeatmap();
+ round.Beatmaps.Add(user);
+ flow.Add(new RoundBeatmapRow(round, user));
+ }
+
+ public class RoundBeatmapRow : CompositeDrawable
+ {
+ public RoundBeatmap Model { get; }
+
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ private readonly Bindable beatmapId = new Bindable();
+
+ private readonly Bindable mods = new Bindable();
+
+ private readonly Container drawableContainer;
+
+ public RoundBeatmapRow(TournamentRound team, RoundBeatmap beatmap)
+ {
+ Model = beatmap;
+
+ Margin = new MarginPadding(10);
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Masking = true;
+ CornerRadius = 5;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ Padding = new MarginPadding { Right = 160 },
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new SettingsTextBox
+ {
+ LabelText = "Beatmap ID",
+ RelativeSizeAxes = Axes.None,
+ Width = 200,
+ Bindable = beatmapId,
+ },
+ new SettingsTextBox
+ {
+ LabelText = "Mods",
+ RelativeSizeAxes = Axes.None,
+ Width = 200,
+ Bindable = mods,
+ },
+ drawableContainer = new Container
+ {
+ Size = new Vector2(100, 70),
+ },
+ }
+ },
+ new DangerousSettingsButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.None,
+ Width = 150,
+ Text = "Delete Beatmap",
+ Action = () =>
+ {
+ Expire();
+ team.Beatmaps.Remove(beatmap);
+ },
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(RulesetStore rulesets)
+ {
+ beatmapId.Value = Model.ID.ToString();
+ beatmapId.BindValueChanged(idString =>
+ {
+ int parsed;
+
+ int.TryParse(idString.NewValue, out parsed);
+
+ Model.ID = parsed;
+
+ if (idString.NewValue != idString.OldValue)
+ Model.BeatmapInfo = null;
+
+ if (Model.BeatmapInfo != null)
+ {
+ updatePanel();
+ return;
+ }
+
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = Model.ID });
+
+ req.Success += res =>
+ {
+ Model.BeatmapInfo = res.ToBeatmap(rulesets);
+ updatePanel();
+ };
+
+ req.Failure += _ =>
+ {
+ Model.BeatmapInfo = null;
+ updatePanel();
+ };
+
+ API.Queue(req);
+ }, true);
+
+ mods.Value = Model.Mods;
+ mods.BindValueChanged(modString => Model.Mods = modString.NewValue);
+ }
+
+ private void updatePanel()
+ {
+ drawableContainer.Clear();
+
+ if (Model.BeatmapInfo != null)
+ drawableContainer.Child = new TournamentBeatmapPanel(Model.BeatmapInfo, Model.Mods)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Width = 300
+ };
+ }
+ }
+ }
+ }
+
+ protected override RoundRow CreateDrawable(TournamentRound model) => new RoundRow(model);
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
new file mode 100644
index 0000000000..93745acabf
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -0,0 +1,314 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Newtonsoft.Json;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Overlays.Settings;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Users;
+using osuTK;
+
+namespace osu.Game.Tournament.Screens.Editors
+{
+ public class TeamEditorScreen : TournamentEditorScreen
+ {
+ [Resolved]
+ private Framework.Game game { get; set; }
+
+ protected override BindableList Storage => LadderInfo.Teams;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ControlPanel.Add(new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = "Add all countries",
+ Action = addAllCountries
+ });
+ }
+
+ protected override TeamRow CreateDrawable(TournamentTeam model) => new TeamRow(model);
+
+ private void addAllCountries()
+ {
+ List countries;
+ using (Stream stream = game.Resources.GetStream("Resources/countries.json"))
+ using (var sr = new StreamReader(stream))
+ countries = JsonConvert.DeserializeObject>(sr.ReadToEnd());
+
+ foreach (var c in countries)
+ Storage.Add(c);
+ }
+
+ public class TeamRow : CompositeDrawable, IModelBacked
+ {
+ public TournamentTeam Model { get; }
+
+ private readonly Container drawableContainer;
+
+ [Resolved]
+ private LadderInfo ladderInfo { get; set; }
+
+ public TeamRow(TournamentTeam team)
+ {
+ Model = team;
+
+ Masking = true;
+ CornerRadius = 10;
+
+ PlayerEditor playerEditor = new PlayerEditor(Model)
+ {
+ Width = 0.95f
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ drawableContainer = new Container
+ {
+ Size = new Vector2(100, 50),
+ Margin = new MarginPadding(10),
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new SettingsTextBox
+ {
+ LabelText = "Name",
+ Width = 0.2f,
+ Bindable = Model.FullName
+ },
+ new SettingsTextBox
+ {
+ LabelText = "Acronym",
+ Width = 0.2f,
+ Bindable = Model.Acronym
+ },
+ new SettingsTextBox
+ {
+ LabelText = "Flag",
+ Width = 0.2f,
+ Bindable = Model.FlagName
+ },
+ new SettingsButton
+ {
+ Width = 0.11f,
+ Margin = new MarginPadding(10),
+ Text = "Add player",
+ Action = () => playerEditor.CreateNew()
+ },
+ new DangerousSettingsButton
+ {
+ Width = 0.11f,
+ Text = "Delete Team",
+ Margin = new MarginPadding(10),
+ Action = () =>
+ {
+ Expire();
+ ladderInfo.Teams.Remove(Model);
+ },
+ },
+ playerEditor
+ }
+ },
+ };
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Model.FlagName.BindValueChanged(updateDrawable, true);
+ }
+
+ private void updateDrawable(ValueChangedEvent flag)
+ {
+ drawableContainer.Child = new DrawableTeamFlag(Model);
+ }
+
+ private class DrawableTeamFlag : DrawableTournamentTeam
+ {
+ public DrawableTeamFlag(TournamentTeam team)
+ : base(team)
+ {
+ InternalChild = Flag;
+ RelativeSizeAxes = Axes.Both;
+
+ Flag.Anchor = Anchor.Centre;
+ Flag.Origin = Anchor.Centre;
+ }
+ }
+
+ public class PlayerEditor : CompositeDrawable
+ {
+ private readonly TournamentTeam team;
+ private readonly FillFlowContainer flow;
+
+ public PlayerEditor(TournamentTeam team)
+ {
+ this.team = team;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = flow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ LayoutDuration = 200,
+ LayoutEasing = Easing.OutQuint,
+ ChildrenEnumerable = team.Players.Select(p => new PlayerRow(team, p))
+ };
+ }
+
+ public void CreateNew()
+ {
+ var user = new User();
+ team.Players.Add(user);
+ flow.Add(new PlayerRow(team, user));
+ }
+
+ public class PlayerRow : CompositeDrawable
+ {
+ private readonly User user;
+
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ private readonly Bindable userId = new Bindable();
+
+ private readonly Container drawableContainer;
+
+ public PlayerRow(TournamentTeam team, User user)
+ {
+ this.user = user;
+
+ Margin = new MarginPadding(10);
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Masking = true;
+ CornerRadius = 5;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ Padding = new MarginPadding { Right = 160 },
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new SettingsTextBox
+ {
+ LabelText = "User ID",
+ RelativeSizeAxes = Axes.None,
+ Width = 200,
+ Bindable = userId,
+ },
+ drawableContainer = new Container
+ {
+ Size = new Vector2(100, 70),
+ },
+ }
+ },
+ new DangerousSettingsButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.None,
+ Width = 150,
+ Text = "Delete Player",
+ Action = () =>
+ {
+ Expire();
+ team.Players.Remove(user);
+ },
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ userId.Value = user.Id.ToString();
+ userId.BindValueChanged(idString =>
+ {
+ long parsed;
+
+ long.TryParse(idString.NewValue, out parsed);
+
+ user.Id = parsed;
+
+ if (idString.NewValue != idString.OldValue)
+ user.Username = string.Empty;
+
+ if (!string.IsNullOrEmpty(user.Username))
+ {
+ updatePanel();
+ return;
+ }
+
+ var req = new GetUserRequest(user.Id);
+
+ req.Success += res =>
+ {
+ // TODO: this should be done in a better way.
+ user.Username = res.Username;
+ user.Country = res.Country;
+ user.Cover = res.Cover;
+
+ updatePanel();
+ };
+
+ req.Failure += _ =>
+ {
+ user.Id = 1;
+ updatePanel();
+ };
+
+ API.Queue(req);
+ }, true);
+ }
+
+ private void updatePanel()
+ {
+ drawableContainer.Child = new UserPanel(user) { Width = 300 };
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
new file mode 100644
index 0000000000..50d3207345
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Tournament.Components;
+using osuTK;
+
+namespace osu.Game.Tournament.Screens.Editors
+{
+ public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo
+ where TDrawable : Drawable, IModelBacked
+ where TModel : class, new()
+ {
+ protected abstract BindableList Storage { get; }
+
+ private FillFlowContainer flow;
+
+ protected ControlPanel ControlPanel;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f),
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.9f,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = flow = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ LayoutDuration = 200,
+ LayoutEasing = Easing.OutQuint,
+ Spacing = new Vector2(20)
+ },
+ },
+ ControlPanel = new ControlPanel
+ {
+ Children = new Drawable[]
+ {
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = "Add new",
+ Action = () => Storage.Add(new TModel())
+ },
+ new DangerousSettingsButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = "Clear all",
+ Action = Storage.Clear
+ },
+ }
+ }
+ });
+
+ Storage.ItemsAdded += items => items.ForEach(i => flow.Add(CreateDrawable(i)));
+ Storage.ItemsRemoved += items => items.ForEach(i => flow.RemoveAll(d => d.Model == i));
+
+ foreach (var model in Storage)
+ flow.Add(CreateDrawable(model));
+ }
+
+ protected abstract TDrawable CreateDrawable(TModel model);
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs
new file mode 100644
index 0000000000..cfa44537d6
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs
@@ -0,0 +1,228 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Showcase;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tournament.Screens.Gameplay.Components
+{
+ public class MatchHeader : Container
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 95;
+ Children = new Drawable[]
+ {
+ new TournamentLogo(),
+ new RoundDisplay
+ {
+ Y = 10,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.TopCentre,
+ },
+ new TeamScoreDisplay(TeamColour.Red)
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ },
+ new TeamScoreDisplay(TeamColour.Blue)
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ };
+ }
+
+ private class TeamScoreDisplay : CompositeDrawable
+ {
+ private readonly TeamColour teamColour;
+
+ private readonly Color4 red = new Color4(129, 68, 65, 255);
+ private readonly Color4 blue = new Color4(41, 91, 97, 255);
+
+ private readonly Bindable currentMatch = new Bindable();
+ private readonly Bindable currentTeam = new Bindable();
+ private readonly Bindable currentTeamScore = new Bindable();
+
+ public TeamScoreDisplay(TeamColour teamColour)
+ {
+ this.teamColour = teamColour;
+
+ RelativeSizeAxes = Axes.Y;
+ Width = 300;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder)
+ {
+ currentMatch.BindValueChanged(matchChanged);
+ currentMatch.BindTo(ladder.CurrentMatch);
+ }
+
+ private void matchChanged(ValueChangedEvent match)
+ {
+ currentTeamScore.UnbindBindings();
+ currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score);
+
+ currentTeam.UnbindBindings();
+ currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2);
+
+ // team may change to same team, which means score is not in a good state.
+ // thus we handle this manually.
+ teamChanged(currentTeam.Value);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ if (currentTeamScore.Value < currentMatch.Value.PointsToWin)
+ currentTeamScore.Value++;
+ return true;
+
+ case MouseButton.Right:
+ if (currentTeamScore.Value > 0)
+ currentTeamScore.Value--;
+ return true;
+ }
+
+ return base.OnMouseDown(e);
+ }
+
+ private void teamChanged(TournamentTeam team)
+ {
+ var colour = teamColour == TeamColour.Red ? red : blue;
+ var flip = teamColour != TeamColour.Red;
+
+ InternalChildren = new Drawable[]
+ {
+ new TeamDisplay(team, colour, flip),
+ new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin)
+ {
+ Colour = colour
+ }
+ };
+ }
+ }
+
+ private class TeamScore : CompositeDrawable
+ {
+ private readonly Bindable currentTeamScore = new Bindable();
+ private readonly StarCounter counter;
+
+ public TeamScore(Bindable score, bool flip, int count)
+ {
+ var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft;
+
+ Anchor = anchor;
+ Origin = anchor;
+
+ InternalChild = counter = new StarCounter(count)
+ {
+ Anchor = anchor,
+ X = (flip ? -1 : 1) * 90,
+ Y = 5,
+ Scale = flip ? new Vector2(-1, 1) : Vector2.One,
+ };
+
+ currentTeamScore.BindValueChanged(scoreChanged);
+ currentTeamScore.BindTo(score);
+ }
+
+ private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0;
+ }
+
+ private class TeamDisplay : DrawableTournamentTeam
+ {
+ public TeamDisplay(TournamentTeam team, Color4 colour, bool flip)
+ : base(team)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft;
+
+ Anchor = Origin = anchor;
+
+ Flag.Anchor = Flag.Origin = anchor;
+ Flag.RelativeSizeAxes = Axes.None;
+ Flag.Size = new Vector2(60, 40);
+ Flag.Margin = new MarginPadding(20);
+
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Flag,
+ new OsuSpriteText
+ {
+ Text = team?.FullName.Value.ToUpper() ?? "???",
+ X = (flip ? -1 : 1) * 90,
+ Y = -10,
+ Colour = colour,
+ Font = TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Regular, size: 20),
+ Origin = anchor,
+ Anchor = anchor,
+ },
+ }
+ };
+ }
+ }
+
+ private class RoundDisplay : CompositeDrawable
+ {
+ private readonly Bindable currentMatch = new Bindable();
+
+ public RoundDisplay()
+ {
+ CornerRadius = 10;
+ Masking = true;
+ Width = 200;
+ Height = 20;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder)
+ {
+ currentMatch.BindValueChanged(matchChanged);
+ currentMatch.BindTo(ladder.CurrentMatch);
+ }
+
+ private void matchChanged(ValueChangedEvent match)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = new Color4(47, 71, 67, 255),
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White,
+ Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round",
+ Font = TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Regular, size: 18),
+ },
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs
new file mode 100644
index 0000000000..78455c8bb7
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs
@@ -0,0 +1,131 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Models;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens.Gameplay.Components
+{
+ public class MatchScoreDisplay : CompositeDrawable
+ {
+ private readonly Color4 red = new Color4(186, 0, 18, 255);
+ private readonly Color4 blue = new Color4(17, 136, 170, 255);
+
+ private const float bar_height = 20;
+
+ private readonly BindableInt score1 = new BindableInt();
+ private readonly BindableInt score2 = new BindableInt();
+
+ private readonly MatchScoreCounter score1Text;
+ private readonly MatchScoreCounter score2Text;
+
+ private readonly Circle score1Bar;
+ private readonly Circle score2Bar;
+
+ public MatchScoreDisplay()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChildren = new Drawable[]
+ {
+ score1Bar = new Circle
+ {
+ Name = "top bar red",
+ RelativeSizeAxes = Axes.X,
+ Height = bar_height,
+ Width = 0,
+ Colour = red,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight
+ },
+ score1Text = new MatchScoreCounter
+ {
+ Colour = red,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ score2Bar = new Circle
+ {
+ Name = "top bar blue",
+ RelativeSizeAxes = Axes.X,
+ Height = bar_height,
+ Width = 0,
+ Colour = blue,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft
+ },
+ score2Text = new MatchScoreCounter
+ {
+ Colour = blue,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder, MatchIPCInfo ipc)
+ {
+ score1.BindValueChanged(_ => updateScores());
+ score1.BindTo(ipc.Score1);
+
+ score2.BindValueChanged(_ => updateScores());
+ score2.BindTo(ipc.Score2);
+ }
+
+ private void updateScores()
+ {
+ score1Text.Current.Value = score1.Value;
+ score2Text.Current.Value = score2.Value;
+
+ var winningText = score1.Value > score2.Value ? score1Text : score2Text;
+ var losingText = score1.Value <= score2.Value ? score1Text : score2Text;
+
+ winningText.Winning = true;
+ losingText.Winning = false;
+
+ var winningBar = score1.Value > score2.Value ? score1Bar : score2Bar;
+ var losingBar = score1.Value <= score2.Value ? score1Bar : score2Bar;
+
+ var diff = Math.Max(score1.Value, score2.Value) - Math.Min(score1.Value, score2.Value);
+
+ losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
+ winningBar.ResizeWidthTo(Math.Min(0.4f, (float)Math.Pow(diff / 1500000f, 0.5) / 2), 400, Easing.OutQuint);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth);
+ score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
+ }
+
+ private class MatchScoreCounter : ScoreCounter
+ {
+ public MatchScoreCounter()
+ {
+ Margin = new MarginPadding { Top = bar_height + 5, Horizontal = 10 };
+
+ Winning = false;
+ }
+
+ public bool Winning
+ {
+ set => DisplayedCountSpriteText.Font = value
+ ? TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Regular, size: 60)
+ : TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Light, size: 40);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
new file mode 100644
index 0000000000..b9a74bfe16
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -0,0 +1,211 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Threading;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Gameplay.Components;
+using osu.Game.Tournament.Screens.MapPool;
+using osu.Game.Tournament.Screens.TeamWin;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens.Gameplay
+{
+ public class GameplayScreen : BeatmapInfoScreen
+ {
+ private readonly BindableBool warmup = new BindableBool();
+
+ private readonly Bindable currentMatch = new Bindable();
+
+ public readonly Bindable State = new Bindable();
+ private OsuButton warmupButton;
+ private MatchIPCInfo ipc;
+
+ private readonly Color4 red = new Color4(186, 0, 18, 255);
+ private readonly Color4 blue = new Color4(17, 136, 170, 255);
+
+ [Resolved(canBeNull: true)]
+ private TournamentSceneManager sceneManager { get; set; }
+
+ [Resolved]
+ private TournamentMatchChatDisplay chat { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(LadderInfo ladder, MatchIPCInfo ipc)
+ {
+ this.ipc = ipc;
+
+ AddRangeInternal(new Drawable[]
+ {
+ new MatchHeader(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Y = 5,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ // chroma key area for stable gameplay
+ Name = "chroma",
+ RelativeSizeAxes = Axes.X,
+ Height = 512,
+ Colour = new Color4(0, 255, 0, 255),
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Y = -4,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ Name = "top bar red",
+ RelativeSizeAxes = Axes.X,
+ Height = 8,
+ Width = 0.5f,
+ Colour = red,
+ },
+ new Circle
+ {
+ Name = "top bar blue",
+ RelativeSizeAxes = Axes.X,
+ Height = 8,
+ Width = 0.5f,
+ Colour = blue,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ }
+ },
+ }
+ },
+ scoreDisplay = new MatchScoreDisplay
+ {
+ Y = -60,
+ Scale = new Vector2(0.8f),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ new ControlPanel
+ {
+ Children = new Drawable[]
+ {
+ warmupButton = new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = "Toggle warmup",
+ Action = () => warmup.Toggle()
+ },
+ new OsuButton
+ {
+ RelativeSizeAxes = Axes.X,
+ Text = "Toggle chat",
+ Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; }
+ }
+ }
+ }
+ });
+
+ State.BindTo(ipc.State);
+ State.BindValueChanged(stateChanged, true);
+
+ currentMatch.BindValueChanged(m =>
+ {
+ warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0;
+ scheduledOperation?.Cancel();
+ });
+
+ currentMatch.BindTo(ladder.CurrentMatch);
+
+ warmup.BindValueChanged(w => warmupButton.Alpha = !w.NewValue ? 0.5f : 1, true);
+ }
+
+ private ScheduledDelegate scheduledOperation;
+ private MatchScoreDisplay scoreDisplay;
+
+ private TourneyState lastState;
+
+ private void stateChanged(ValueChangedEvent state)
+ {
+ try
+ {
+ if (state.NewValue == TourneyState.Ranking)
+ {
+ if (warmup.Value) return;
+
+ if (ipc.Score1.Value > ipc.Score2.Value)
+ currentMatch.Value.Team1Score.Value++;
+ else
+ currentMatch.Value.Team2Score.Value++;
+ }
+
+ scheduledOperation?.Cancel();
+
+ void expand()
+ {
+ chat?.Expand();
+
+ using (BeginDelayedSequence(300, true))
+ {
+ scoreDisplay.FadeIn(100);
+ SongBar.Expanded = true;
+ }
+ }
+
+ void contract()
+ {
+ SongBar.Expanded = false;
+ scoreDisplay.FadeOut(100);
+ using (chat?.BeginDelayedSequence(500))
+ chat?.Contract();
+ }
+
+ switch (state.NewValue)
+ {
+ case TourneyState.Idle:
+ contract();
+
+ const float delay_before_progression = 4000;
+
+ // if we've returned to idle and the last screen was ranking
+ // we should automatically proceed after a short delay
+ if (lastState == TourneyState.Ranking && !warmup.Value)
+ {
+ if (currentMatch.Value?.Completed.Value == true)
+ scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression);
+ else if (currentMatch.Value?.Completed.Value == false)
+ scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression);
+ }
+
+ break;
+
+ case TourneyState.Ranking:
+ scheduledOperation = Scheduler.AddDelayed(contract, 10000);
+ break;
+
+ default:
+ chat.Expand();
+ expand();
+ break;
+ }
+ }
+ finally
+ {
+ lastState = state.NewValue;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/IProvideVideo.cs b/osu.Game.Tournament/Screens/IProvideVideo.cs
new file mode 100644
index 0000000000..61989d8448
--- /dev/null
+++ b/osu.Game.Tournament/Screens/IProvideVideo.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Tournament.Screens
+{
+ ///
+ /// Marker interface for a screen which provides its own local video background.
+ ///
+ public interface IProvideVideo
+ {
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs
new file mode 100644
index 0000000000..16224a7fb4
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ ///
+ /// A match that may not necessarily occur.
+ ///
+ public class ConditionalTournamentMatch : TournamentMatch
+ {
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs
new file mode 100644
index 0000000000..ded21730f3
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs
@@ -0,0 +1,207 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Editors;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ public class DrawableMatchTeam : DrawableTournamentTeam, IHasContextMenu
+ {
+ private readonly TournamentMatch match;
+ private readonly bool losers;
+ private OsuSpriteText scoreText;
+ private Box background;
+
+ private readonly Bindable score = new Bindable();
+ private readonly BindableBool completed = new BindableBool();
+
+ private Color4 colourWinner;
+ private Color4 colourNormal;
+
+ private readonly Func isWinner;
+ private LadderEditorScreen ladderEditor;
+
+ [Resolved]
+ private LadderInfo ladderInfo { get; set; }
+
+ private void setCurrent()
+ {
+ //todo: tournamentgamebase?
+ if (ladderInfo.CurrentMatch.Value != null)
+ ladderInfo.CurrentMatch.Value.Current.Value = false;
+
+ ladderInfo.CurrentMatch.Value = match;
+ ladderInfo.CurrentMatch.Value.Current.Value = true;
+ }
+
+ [Resolved(CanBeNull = true)]
+ private LadderEditorInfo editorInfo { get; set; }
+
+ public DrawableMatchTeam(TournamentTeam team, TournamentMatch match, bool losers)
+ : base(team)
+ {
+ this.match = match;
+ this.losers = losers;
+ Size = new Vector2(150, 40);
+
+ Masking = true;
+ CornerRadius = 5;
+
+ Flag.Scale = new Vector2(0.9f);
+ Flag.Anchor = Flag.Origin = Anchor.CentreLeft;
+
+ AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft;
+ AcronymText.Padding = new MarginPadding { Left = 50 };
+ AcronymText.Font = OsuFont.GetFont(size: 24);
+
+ if (match != null)
+ {
+ isWinner = () => match.Winner == Team;
+
+ completed.BindTo(match.Completed);
+ if (team != null)
+ score.BindTo(team == match.Team1.Value ? match.Team1Score : match.Team2Score);
+ }
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuColour colours, LadderEditorScreen ladderEditor)
+ {
+ this.ladderEditor = ladderEditor;
+
+ colourWinner = losers ? colours.YellowDarker : colours.BlueDarker;
+ colourNormal = OsuColour.Gray(0.2f);
+
+ InternalChildren = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Padding = new MarginPadding(5),
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ AcronymText,
+ Flag,
+ new Container
+ {
+ Masking = true,
+ CornerRadius = 5,
+ Width = 0.3f,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ Alpha = 0.8f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ scoreText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(size: 20),
+ }
+ }
+ }
+ }
+ }
+ };
+
+ completed.BindValueChanged(_ => updateWinStyle());
+
+ score.BindValueChanged(val =>
+ {
+ scoreText.Text = val.NewValue?.ToString() ?? string.Empty;
+ updateWinStyle();
+ }, true);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (Team == null || editorInfo != null) return false;
+
+ if (!match.Current.Value)
+ {
+ setCurrent();
+ return true;
+ }
+
+ if (e.Button == MouseButton.Left)
+ {
+ if (score.Value == null)
+ {
+ match.StartMatch();
+ }
+ else if (!match.Completed.Value)
+ score.Value++;
+ }
+ else
+ {
+ if (match.Progression.Value?.Completed.Value == true)
+ // don't allow changing scores if the match has a progression. can cause large data loss
+ return false;
+
+ if (match.Completed.Value && match.Winner != Team)
+ // don't allow changing scores from the non-winner
+ return false;
+
+ if (score.Value > 0)
+ score.Value--;
+ else
+ match.CancelMatchStart();
+ }
+
+ return false;
+ }
+
+ private void updateWinStyle()
+ {
+ bool winner = completed.Value && isWinner?.Invoke() == true;
+
+ background.FadeColour(winner ? colourWinner : colourNormal, winner ? 500 : 0, Easing.OutQuint);
+
+ scoreText.Font = AcronymText.Font = OsuFont.GetFont(weight: winner ? FontWeight.Bold : FontWeight.Regular);
+ }
+
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ if (editorInfo == null)
+ return new MenuItem[0];
+
+ return new MenuItem[]
+ {
+ new OsuMenuItem("Set as current", MenuItemType.Standard, setCurrent),
+ new OsuMenuItem("Join with", MenuItemType.Standard, () => ladderEditor.BeginJoin(match, false)),
+ new OsuMenuItem("Join with (loser)", MenuItemType.Standard, () => ladderEditor.BeginJoin(match, true)),
+ new OsuMenuItem("Remove", MenuItemType.Destructive, () => ladderEditor.Remove(match)),
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs
new file mode 100644
index 0000000000..dde280ccd8
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs
@@ -0,0 +1,313 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Tournament.Models;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+using SixLabors.Primitives;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ public class DrawableTournamentMatch : CompositeDrawable
+ {
+ public readonly TournamentMatch Match;
+ private readonly bool editor;
+ protected readonly FillFlowContainer Flow;
+ private readonly Drawable selectionBox;
+ private readonly Drawable currentMatchSelectionBox;
+ private Bindable globalSelection;
+
+ [Resolved(CanBeNull = true)]
+ private LadderEditorInfo editorInfo { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private LadderInfo ladderInfo { get; set; }
+
+ public DrawableTournamentMatch(TournamentMatch match, bool editor = false)
+ {
+ Match = match;
+ this.editor = editor;
+
+ AutoSizeAxes = Axes.Both;
+
+ Margin = new MarginPadding(5);
+
+ InternalChildren = new[]
+ {
+ selectionBox = new Container
+ {
+ CornerRadius = 5,
+ Masking = true,
+ Scale = new Vector2(1.05f),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ Colour = Color4.YellowGreen,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ currentMatchSelectionBox = new Container
+ {
+ CornerRadius = 5,
+ Masking = true,
+ Scale = new Vector2(1.05f),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ Colour = Color4.OrangeRed,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ Flow = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(2)
+ }
+ };
+
+ boundReference(match.Team1).BindValueChanged(_ => updateTeams());
+ boundReference(match.Team2).BindValueChanged(_ => updateTeams());
+ boundReference(match.Team1Score).BindValueChanged(_ => updateWinConditions());
+ boundReference(match.Team2Score).BindValueChanged(_ => updateWinConditions());
+ boundReference(match.Round).BindValueChanged(_ =>
+ {
+ updateWinConditions();
+ Changed?.Invoke();
+ });
+ boundReference(match.Completed).BindValueChanged(_ => updateProgression());
+ boundReference(match.Progression).BindValueChanged(_ => updateProgression());
+ boundReference(match.LosersProgression).BindValueChanged(_ => updateProgression());
+ boundReference(match.Losers).BindValueChanged(_ =>
+ {
+ updateTeams();
+ Changed?.Invoke();
+ });
+ boundReference(match.Current).BindValueChanged(_ => updateCurrentMatch(), true);
+ boundReference(match.Position).BindValueChanged(pos =>
+ {
+ if (!IsDragged)
+ Position = new Vector2(pos.NewValue.X, pos.NewValue.Y);
+ Changed?.Invoke();
+ }, true);
+
+ updateTeams();
+ }
+
+ ///
+ /// Fired when somethign changed that requires a ladder redraw.
+ ///
+ public Action Changed;
+
+ private readonly List refBindables = new List();
+
+ private T boundReference(T obj)
+ where T : IBindable
+ {
+ obj = (T)obj.GetBoundCopy();
+ refBindables.Add(obj);
+ return obj;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ foreach (var b in refBindables)
+ b.UnbindAll();
+ }
+
+ private void updateCurrentMatch()
+ {
+ if (Match.Current.Value)
+ currentMatchSelectionBox.Show();
+ else
+ currentMatchSelectionBox.Hide();
+ }
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+
+ set
+ {
+ if (value == selected) return;
+
+ selected = value;
+
+ if (selected)
+ {
+ selectionBox.Show();
+ if (editor)
+ editorInfo.Selected.Value = Match;
+ else
+ ladderInfo.CurrentMatch.Value = Match;
+ }
+ else
+ selectionBox.Hide();
+ }
+ }
+
+ private void updateProgression()
+ {
+ if (!Match.Completed.Value)
+ {
+ // ensure we clear any of our teams from our progression.
+ // this is not pretty logic but should suffice for now.
+ if (Match.Progression.Value != null && Match.Progression.Value.Team1.Value == Match.Team1.Value)
+ Match.Progression.Value.Team1.Value = null;
+
+ if (Match.Progression.Value != null && Match.Progression.Value.Team2.Value == Match.Team2.Value)
+ Match.Progression.Value.Team2.Value = null;
+
+ if (Match.LosersProgression.Value != null && Match.LosersProgression.Value.Team1.Value == Match.Team1.Value)
+ Match.LosersProgression.Value.Team1.Value = null;
+
+ if (Match.LosersProgression.Value != null && Match.LosersProgression.Value.Team2.Value == Match.Team2.Value)
+ Match.LosersProgression.Value.Team2.Value = null;
+ }
+ else
+ {
+ transferProgression(Match.Progression?.Value, Match.Winner);
+ transferProgression(Match.LosersProgression?.Value, Match.Loser);
+ }
+
+ Changed?.Invoke();
+ }
+
+ private void transferProgression(TournamentMatch destination, TournamentTeam team)
+ {
+ if (destination == null) return;
+
+ bool progressionAbove = destination.ID < Match.ID;
+
+ Bindable destinationTeam;
+
+ // check for the case where we have already transferred out value
+ if (destination.Team1.Value == team)
+ destinationTeam = destination.Team1;
+ else if (destination.Team2.Value == team)
+ destinationTeam = destination.Team2;
+ else
+ {
+ destinationTeam = progressionAbove ? destination.Team2 : destination.Team1;
+ if (destinationTeam.Value != null)
+ destinationTeam = progressionAbove ? destination.Team1 : destination.Team2;
+ }
+
+ destinationTeam.Value = team;
+ }
+
+ private void updateWinConditions()
+ {
+ if (Match.Round.Value == null) return;
+
+ var instaWinAmount = Match.Round.Value.BestOf.Value / 2;
+
+ Match.Completed.Value = Match.Round.Value.BestOf.Value > 0
+ && (Match.Team1Score.Value + Match.Team2Score.Value >= Match.Round.Value.BestOf.Value || Match.Team1Score.Value > instaWinAmount || Match.Team2Score.Value > instaWinAmount);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateTeams();
+
+ if (editorInfo != null)
+ {
+ globalSelection = editorInfo.Selected.GetBoundCopy();
+ globalSelection.BindValueChanged(s =>
+ {
+ if (s.NewValue != Match) Selected = false;
+ });
+ }
+ }
+
+ private void updateTeams()
+ {
+ if (LoadState != LoadState.Loaded)
+ return;
+
+ // todo: teams may need to be bindable for transitions at a later point.
+
+ if (Match.Team1.Value == null || Match.Team2.Value == null)
+ Match.CancelMatchStart();
+
+ if (Match.ConditionalMatches.Count > 0)
+ {
+ foreach (var conditional in Match.ConditionalMatches)
+ {
+ var team1Match = conditional.Acronyms.Contains(Match.Team1Acronym);
+ var team2Match = conditional.Acronyms.Contains(Match.Team2Acronym);
+
+ if (team1Match && team2Match)
+ Match.Date.Value = conditional.Date.Value;
+ }
+ }
+
+ Flow.Children = new[]
+ {
+ new DrawableMatchTeam(Match.Team1.Value, Match, Match.Losers.Value),
+ new DrawableMatchTeam(Match.Team2.Value, Match, Match.Losers.Value)
+ };
+
+ SchedulerAfterChildren.Add(() => Scheduler.Add(updateProgression));
+ updateWinConditions();
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left && editorInfo != null;
+
+ protected override bool OnDragStart(DragStartEvent e) => editorInfo != null;
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (Selected && editorInfo != null && e.Key == Key.Delete)
+ {
+ Remove();
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (editorInfo == null || Match is ConditionalTournamentMatch)
+ return false;
+
+ Selected = true;
+ return true;
+ }
+
+ protected override bool OnDrag(DragEvent e)
+ {
+ if (base.OnDrag(e)) return true;
+
+ Selected = true;
+ this.MoveToOffset(e.Delta);
+
+ var pos = Position;
+ Match.Position.Value = new Point((int)pos.X, (int)pos.Y);
+ return true;
+ }
+
+ public void Remove()
+ {
+ Selected = false;
+ Match.Progression.Value = null;
+ Match.LosersProgression.Value = null;
+
+ ladderInfo.Matches.Remove(Match);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs
new file mode 100644
index 0000000000..67d6bc4fa6
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs
@@ -0,0 +1,58 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Tournament.Models;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ public class DrawableTournamentRound : CompositeDrawable
+ {
+ [UsedImplicitly]
+ private readonly Bindable name;
+
+ [UsedImplicitly]
+ private readonly Bindable description;
+
+ public DrawableTournamentRound(TournamentRound round, bool losers = false)
+ {
+ OsuSpriteText textName;
+ OsuSpriteText textDescription;
+
+ AutoSizeAxes = Axes.Both;
+ InternalChild = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ textDescription = new OsuSpriteText
+ {
+ Colour = Color4.Black,
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre
+ },
+ textName = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(weight: FontWeight.Bold),
+ Colour = Color4.Black,
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre
+ },
+ }
+ };
+
+ name = round.Name.GetBoundCopy();
+ name.BindValueChanged(n => textName.Text = ((losers ? "Losers " : "") + round.Name).ToUpper(), true);
+
+ description = round.Name.GetBoundCopy();
+ description.BindValueChanged(n => textDescription.Text = round.Description.Value.ToUpper(), true);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
new file mode 100644
index 0000000000..0864d25a2f
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -0,0 +1,168 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Screens.Play.PlayerSettings;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ public class LadderEditorSettings : PlayerSettingsGroup
+ {
+ private const int padding = 10;
+
+ protected override string Title => @"ladder";
+
+ private SettingsDropdown roundDropdown;
+ private PlayerCheckbox losersCheckbox;
+ private DateTextBox dateTimeBox;
+ private SettingsTeamDropdown team1Dropdown;
+ private SettingsTeamDropdown team2Dropdown;
+
+ [Resolved]
+ private LadderEditorInfo editorInfo { get; set; }
+
+ [Resolved]
+ private LadderInfo ladderInfo { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ team1Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 1" },
+ team2Dropdown = new SettingsTeamDropdown(ladderInfo.Teams) { LabelText = "Team 2" },
+ roundDropdown = new SettingsRoundDropdown(ladderInfo.Rounds) { LabelText = "Round" },
+ losersCheckbox = new PlayerCheckbox { LabelText = "Losers Bracket" },
+ dateTimeBox = new DateTextBox { LabelText = "Match Time" },
+ };
+
+ editorInfo.Selected.ValueChanged += selection =>
+ {
+ roundDropdown.Bindable = selection.NewValue?.Round;
+ losersCheckbox.Current = selection.NewValue?.Losers;
+ dateTimeBox.Bindable = selection.NewValue?.Date;
+
+ team1Dropdown.Bindable = selection.NewValue?.Team1;
+ team2Dropdown.Bindable = selection.NewValue?.Team2;
+ };
+
+ roundDropdown.Bindable.ValueChanged += round =>
+ {
+ if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value)
+ {
+ editorInfo.Selected.Value.Date.Value = round.NewValue.StartDate.Value;
+ editorInfo.Selected.TriggerChange();
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ this.FadeIn();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ return false;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ }
+
+ private class SettingsRoundDropdown : LadderSettingsDropdown
+ {
+ public SettingsRoundDropdown(BindableList rounds)
+ {
+ Bindable = new Bindable();
+
+ foreach (var r in rounds.Prepend(new TournamentRound()))
+ add(r);
+
+ rounds.ItemsRemoved += items => items.ForEach(i => Control.RemoveDropdownItem(i));
+ rounds.ItemsAdded += items => items.ForEach(add);
+ }
+
+ private readonly List refBindables = new List();
+
+ private T boundReference(T obj)
+ where T : IBindable
+ {
+ obj = (T)obj.GetBoundCopy();
+ refBindables.Add(obj);
+ return obj;
+ }
+
+ private void add(TournamentRound round)
+ {
+ Control.AddDropdownItem(round);
+ boundReference(round.Name).BindValueChanged(_ =>
+ {
+ Control.RemoveDropdownItem(round);
+ Control.AddDropdownItem(round);
+ });
+ }
+ }
+
+ private class SettingsTeamDropdown : LadderSettingsDropdown
+ {
+ public SettingsTeamDropdown(BindableList teams)
+ {
+ foreach (var t in teams.Prepend(new TournamentTeam()))
+ add(t);
+
+ teams.ItemsRemoved += items => items.ForEach(i => Control.RemoveDropdownItem(i));
+ teams.ItemsAdded += items => items.ForEach(add);
+ }
+
+ private readonly List refBindables = new List();
+
+ private T boundReference(T obj)
+ where T : IBindable
+ {
+ obj = (T)obj.GetBoundCopy();
+ refBindables.Add(obj);
+ return obj;
+ }
+
+ private void add(TournamentTeam team)
+ {
+ Control.AddDropdownItem(team);
+ boundReference(team.FullName).BindValueChanged(_ =>
+ {
+ Control.RemoveDropdownItem(team);
+ Control.AddDropdownItem(team);
+ });
+ }
+ }
+
+ private class LadderSettingsDropdown : SettingsDropdown
+ {
+ protected override OsuDropdown CreateDropdown() => new DropdownControl();
+
+ private new class DropdownControl : SettingsDropdown.DropdownControl
+ {
+ protected override DropdownMenu CreateMenu() => new Menu();
+
+ private new class Menu : OsuDropdownMenu
+ {
+ public Menu()
+ {
+ MaxHeight = 200;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs
new file mode 100644
index 0000000000..34e0dc770f
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/Components/ProgressionPath.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Lines;
+using osuTK;
+
+namespace osu.Game.Tournament.Screens.Ladder.Components
+{
+ public class ProgressionPath : Path
+ {
+ public DrawableTournamentMatch Source { get; private set; }
+ public DrawableTournamentMatch Destination { get; private set; }
+
+ public ProgressionPath(DrawableTournamentMatch source, DrawableTournamentMatch destination)
+ {
+ Source = source;
+ Destination = destination;
+
+ PathRadius = 3;
+ BypassAutoSizeAxes = Axes.Both;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Vector2 getCenteredVector(Vector2 top, Vector2 bottom) => new Vector2(top.X, top.Y + (bottom.Y - top.Y) / 2);
+
+ var q1 = Source.ScreenSpaceDrawQuad;
+ var q2 = Destination.ScreenSpaceDrawQuad;
+
+ float padding = q1.Width / 20;
+
+ bool progressionToRight = q2.TopLeft.X > q1.TopLeft.X;
+
+ if (!progressionToRight)
+ {
+ var temp = q2;
+ q2 = q1;
+ q1 = temp;
+ }
+
+ var c1 = getCenteredVector(q1.TopRight, q1.BottomRight) + new Vector2(padding, 0);
+ var c2 = getCenteredVector(q2.TopLeft, q2.BottomLeft) - new Vector2(padding, 0);
+
+ var p1 = c1;
+ var p2 = p1 + new Vector2(padding, 0);
+
+ if (p2.X > c2.X)
+ {
+ c2 = getCenteredVector(q2.TopRight, q2.BottomRight) + new Vector2(padding, 0);
+ p2.X = c2.X + padding;
+ }
+
+ var p3 = new Vector2(p2.X, c2.Y);
+ var p4 = new Vector2(c2.X, p3.Y);
+
+ var points = new[] { p1, p2, p3, p4 };
+
+ float minX = points.Min(p => p.X);
+ float minY = points.Min(p => p.Y);
+
+ var topLeft = new Vector2(minX, minY);
+
+ Position = Parent.ToLocalSpace(topLeft);
+ Vertices = points.Select(p => Parent.ToLocalSpace(p) - Parent.ToLocalSpace(topLeft)).ToList();
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
new file mode 100644
index 0000000000..f613ce5f46
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Input.Events;
+using osuTK;
+
+namespace osu.Game.Tournament.Screens.Ladder
+{
+ public class LadderDragContainer : Container
+ {
+ protected override bool OnDragStart(DragStartEvent e) => true;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
+ private Vector2 target;
+
+ private float scale = 1;
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+
+ protected override bool OnDrag(DragEvent e)
+ {
+ this.MoveTo(target += e.Delta, 1000, Easing.OutQuint);
+ return true;
+ }
+
+ private const float min_scale = 0.6f;
+ private const float max_scale = 1.4f;
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ var newScale = MathHelper.Clamp(scale + e.ScrollDelta.Y / 15 * scale, min_scale, max_scale);
+
+ this.MoveTo(target = target - e.MousePosition * (newScale - scale), 2000, Easing.OutQuint);
+ this.ScaleTo(scale = newScale, 2000, Easing.OutQuint);
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
new file mode 100644
index 0000000000..67531ce5d3
--- /dev/null
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -0,0 +1,174 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Platform;
+using osu.Game.Graphics;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Editors;
+using osu.Game.Tournament.Screens.Ladder.Components;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens.Ladder
+{
+ public class LadderScreen : TournamentScreen, IProvideVideo
+ {
+ protected Container MatchesContainer;
+ private Container paths;
+ private Container headings;
+
+ protected LadderDragContainer ScrollContent;
+
+ protected Container Content;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, Storage storage)
+ {
+ normalPathColour = colours.BlueDarker.Darken(2);
+ losersPathColour = colours.YellowDarker.Darken(2);
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new TourneyVideo(storage.GetStream(@"BG Side Logo - OWC.m4v"))
+ {
+ RelativeSizeAxes = Axes.Both,
+ Loop = true,
+ },
+ ScrollContent = new LadderDragContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ paths = new Container { RelativeSizeAxes = Axes.Both },
+ headings = new Container { RelativeSizeAxes = Axes.Both },
+ MatchesContainer = new Container { RelativeSizeAxes = Axes.Both },
+ }
+ },
+ }
+ };
+
+ void addMatch(TournamentMatch match) =>
+ MatchesContainer.Add(new DrawableTournamentMatch(match, this is LadderEditorScreen)
+ {
+ Changed = () => layout.Invalidate()
+ });
+
+ foreach (var match in LadderInfo.Matches)
+ addMatch(match);
+
+ LadderInfo.Rounds.ItemsAdded += _ => layout.Invalidate();
+ LadderInfo.Rounds.ItemsRemoved += _ => layout.Invalidate();
+
+ LadderInfo.Matches.ItemsAdded += matches =>
+ {
+ foreach (var p in matches)
+ addMatch(p);
+ layout.Invalidate();
+ };
+
+ LadderInfo.Matches.ItemsRemoved += matches =>
+ {
+ foreach (var p in matches)
+ foreach (var d in MatchesContainer.Where(d => d.Match == p))
+ d.Expire();
+
+ layout.Invalidate();
+ };
+ }
+
+ private Cached layout = new Cached();
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!layout.IsValid)
+ UpdateLayout();
+ }
+
+ private Color4 normalPathColour;
+ private Color4 losersPathColour;
+
+ protected virtual bool DrawLoserPaths => false;
+
+ protected virtual void UpdateLayout()
+ {
+ paths.Clear();
+ headings.Clear();
+
+ int id = 1;
+
+ foreach (var match in MatchesContainer.OrderBy(d => d.Y).ThenBy(d => d.X))
+ {
+ match.Match.ID = id++;
+
+ if (match.Match.Progression.Value != null)
+ {
+ var dest = MatchesContainer.FirstOrDefault(p => p.Match == match.Match.Progression.Value);
+
+ if (dest == null)
+ // clean up outdated progressions.
+ match.Match.Progression.Value = null;
+ else
+ paths.Add(new ProgressionPath(match, dest) { Colour = match.Match.Losers.Value ? losersPathColour : normalPathColour });
+ }
+
+ if (DrawLoserPaths)
+ {
+ if (match.Match.LosersProgression.Value != null)
+ {
+ var dest = MatchesContainer.FirstOrDefault(p => p.Match == match.Match.LosersProgression.Value);
+
+ if (dest == null)
+ // clean up outdated progressions.
+ match.Match.LosersProgression.Value = null;
+ else
+ paths.Add(new ProgressionPath(match, dest) { Colour = losersPathColour.Opacity(0.1f) });
+ }
+ }
+ }
+
+ foreach (var round in LadderInfo.Rounds)
+ {
+ var topMatch = MatchesContainer.Where(p => !p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+
+ if (topMatch == null) continue;
+
+ headings.Add(new DrawableTournamentRound(round)
+ {
+ Position = headings.ToLocalSpace((topMatch.ScreenSpaceDrawQuad.TopLeft + topMatch.ScreenSpaceDrawQuad.TopRight) / 2),
+ Margin = new MarginPadding { Bottom = 10 },
+ Origin = Anchor.BottomCentre,
+ });
+ }
+
+ foreach (var round in LadderInfo.Rounds)
+ {
+ var topMatch = MatchesContainer.Where(p => p.Match.Losers.Value && p.Match.Round.Value == round).OrderBy(p => p.Y).FirstOrDefault();
+
+ if (topMatch == null) continue;
+
+ headings.Add(new DrawableTournamentRound(round, true)
+ {
+ Position = headings.ToLocalSpace((topMatch.ScreenSpaceDrawQuad.TopLeft + topMatch.ScreenSpaceDrawQuad.TopRight) / 2),
+ Margin = new MarginPadding { Bottom = 10 },
+ Origin = Anchor.BottomCentre,
+ });
+ }
+
+ layout.Validate();
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
new file mode 100644
index 0000000000..46d4cfa98c
--- /dev/null
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -0,0 +1,239 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Components;
+using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.Models;
+using osu.Game.Tournament.Screens.Gameplay;
+using osu.Game.Tournament.Screens.Gameplay.Components;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tournament.Screens.MapPool
+{
+ public class MapPoolScreen : TournamentScreen
+ {
+ private readonly FillFlowContainer