diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f9f8ce3eea2c..1771459a878b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -182,6 +182,8 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
+ - name: Set Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_26.3.app
- name: Install .NET Workloads
run: dotnet workload install ios
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 63b69ac6b607..81c2e6ccc71d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,8 +3,8 @@ name: Build Android APK
on:
push:
tags:
- - "*.*.*"
- - "!*-*"
+ - '*.*.*'
+ - '!*-*'
workflow_dispatch:
jobs:
@@ -31,9 +31,6 @@ jobs:
with:
dotnet-version: "10.0.x"
- - name: Update .NET workloads
- run: dotnet workload update
-
- name: Install .NET Android workload
run: dotnet workload install android
@@ -91,49 +88,38 @@ jobs:
- name: Build Android APK (signed)
if: steps.keystore.outputs.has_keystore == 'true'
- env:
- ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
- KEYPASS: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
- STOREPASS: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
- run: |
- dotnet publish -c Release \
- osu.Android/osu.Android.csproj \
- -f net10.0-android \
- -p:Version="${{ steps.version.outputs.version }}" \
- -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \
- -p:ApplicationVersion="${{ github.run_number }}" \
- -p:AndroidKeyStore="true" \
- -p:AndroidSigningKeyStore="${{ github.workspace }}/osu.Android/osu.keystore" \
- -p:AndroidSigningKeyAlias="${ALIAS}" \
- -p:AndroidSigningKeyPass="${KEYPASS}" \
- -p:AndroidSigningStorePass="${STOREPASS}"
+ run: >
+ dotnet publish -c Release
+ osu.Android/osu.Android.csproj
+ -f net10.0-android
+ -p:Version=${{ steps.version.outputs.version }}
+ -p:ApplicationDisplayVersion=${{ steps.version.outputs.version }}
+ -p:ApplicationVersion=${{ github.run_number }}
+ -p:AndroidKeyStore=true
+ -p:AndroidSigningKeyStore=${{ github.workspace }}/osu.Android/osu.keystore
+ -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
+ -p:AndroidSigningKeyPass=${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
+ -p:AndroidSigningStorePass=${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
- name: Build Android APK (unsigned)
if: steps.keystore.outputs.has_keystore != 'true'
- run: |
- dotnet publish -c Release \
- osu.Android/osu.Android.csproj \
- -f net10.0-android \
- -p:Version="${{ steps.version.outputs.version }}" \
- -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \
- -p:ApplicationVersion="${{ github.run_number }}" \
- -p:AndroidKeyStore="false"
+ run: >
+ dotnet publish -c Release
+ osu.Android/osu.Android.csproj
+ -f net10.0-android
+ -p:Version=${{ steps.version.outputs.version }}
+ -p:ApplicationDisplayVersion=${{ steps.version.outputs.version }}
+ -p:ApplicationVersion=${{ github.run_number }}
+ -p:AndroidKeyStore=false
- name: Find APK
id: find_apk
run: |
- # Strictly prioritize the publish/ directory where the signed APK resides
PUBLISH_DIR="osu.Android/bin/Release/net10.0-android/publish"
- echo "Searching for APK in: $PUBLISH_DIR"
APK=$(find "$PUBLISH_DIR" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)
-
if [ -z "$APK" ]; then
- echo "::error::Signed APK not found in publish directory ($PUBLISH_DIR)!"
- # Use a non-restricted way to fail
- sh -c "false"
+ APK=$(find osu.Android/bin/Release -name "*.apk" | head -1)
fi
-
- echo "Final APK found at: $APK"
echo "apk_path=$APK" >> "$GITHUB_OUTPUT"
- name: Upload APK artifact
diff --git a/build/PatchElfPageSize.targets b/build/PatchElfPageSize.targets
index 815e1b4aacd6..9be15f4da475 100644
--- a/build/PatchElfPageSize.targets
+++ b/build/PatchElfPageSize.targets
@@ -1,3 +1,17 @@
+
@@ -7,131 +21,250 @@
0 ? TargetPageSize : 0x4000;
+WasPatched = false;
+int pageSize = TargetPageSize > 0 ? TargetPageSize : 0x4000; // 16 KB default
+
+if (!System.IO.File.Exists(FilePath))
+ return true;
+
+byte[] data = System.IO.File.ReadAllBytes(FilePath);
+
+// ── Validate ELF magic ──────────────────────────────────────────────
+if (data.Length < 64 || data[0] != 0x7F || data[1] != (byte)'E' || data[2] != (byte)'L' || data[3] != (byte)'F')
+ return true;
+
+int eiClass = data[4]; // 1 = 32-bit, 2 = 64-bit
+bool is64 = eiClass == 2;
+bool isLE = data[5] == 1;
+
+// Only 64-bit little-endian ELFs need 16 KB alignment (Android arm64/x64).
+if (!is64 || !isLE)
+ return true;
+
+// ── Helper lambdas ──────────────────────────────────────────────────
+System.Func RU64 = (b, o) =>
+ (ulong)b[o] | ((ulong)b[o+1] << 8) | ((ulong)b[o+2] << 16) | ((ulong)b[o+3] << 24) |
+ ((ulong)b[o+4] << 32) | ((ulong)b[o+5] << 40) | ((ulong)b[o+6] << 48) | ((ulong)b[o+7] << 56);
+
+System.Func RU32 = (b, o) =>
+ (uint)b[o] | ((uint)b[o+1] << 8) | ((uint)b[o+2] << 16) | ((uint)b[o+3] << 24);
+
+System.Action WU64 = (b, o, v) =>
+{
+ b[o] = (byte)(v); b[o+1] = (byte)(v >> 8);
+ b[o+2] = (byte)(v >> 16); b[o+3] = (byte)(v >> 24);
+ b[o+4] = (byte)(v >> 32); b[o+5] = (byte)(v >> 40);
+ b[o+6] = (byte)(v >> 48); b[o+7] = (byte)(v >> 56);
+};
+
+// ── Parse ELF header ────────────────────────────────────────────────
+ulong e_phoff = RU64(data, 32);
+ulong e_shoff = RU64(data, 40);
+int e_phentsize = (int)(data[54] | (data[55] << 8));
+int e_phnum = (int)(data[56] | (data[57] << 8));
+int e_shentsize = (int)(data[58] | (data[59] << 8));
+int e_shnum = (int)(data[60] | (data[61] << 8));
+
+// ── Collect LOAD segments ───────────────────────────────────────────
+const uint PT_LOAD = 1;
+bool alreadyAligned = true;
+var loadIndices = new System.Collections.Generic.List();
+
+for (int i = 0; i < e_phnum; i++)
+{
+ int hdr = (int)e_phoff + i * e_phentsize;
+ if (RU32(data, hdr) == PT_LOAD)
+ {
+ loadIndices.Add(i);
+ if (RU64(data, hdr + 48) < (ulong)pageSize)
+ alreadyAligned = false;
+ }
+}
+
+if (alreadyAligned)
+ return true;
+
+// ── Build a new file with proper padding ────────────────────────────
+var segInfos = new System.Collections.Generic.List>();
+foreach (int idx in loadIndices)
+{
+ int hdr = (int)e_phoff + idx * e_phentsize;
+ segInfos.Add(System.Tuple.Create(idx,
+ RU64(data, hdr + 8), // p_offset
+ RU64(data, hdr + 32), // p_filesz
+ RU64(data, hdr + 16) // p_vaddr
+ ));
+}
+segInfos.Sort((a, b) => a.Item2.CompareTo(b.Item2));
+
+var ms = new System.IO.MemoryStream(data.Length + pageSize * loadIndices.Count);
+int currentOld = 0;
+var deltas = new System.Collections.Generic.List>();
+
+foreach (var seg in segInfos)
+{
+ int oldOff = (int)seg.Item2;
+ ulong vaddr = seg.Item4;
+ int filesz = (int)seg.Item3;
+
+ if (oldOff > currentOld)
+ ms.Write(data, currentOld, oldOff - currentOld);
+
+ long newOff = ms.Position;
+ ulong requiredMod = vaddr % (ulong)pageSize;
+ ulong currentMod = (ulong)newOff % (ulong)pageSize;
+
+ if (currentMod != requiredMod)
+ {
+ long pad = (requiredMod >= currentMod)
+ ? (long)(requiredMod - currentMod)
+ : (long)((ulong)pageSize - currentMod + requiredMod);
+ for (long p = 0; p < pad; p++) ms.WriteByte(0);
+ newOff = ms.Position;
+ }
+
+ deltas.Add(System.Tuple.Create((ulong)oldOff, newOff - oldOff));
+ ms.Write(data, oldOff, filesz);
+ currentOld = oldOff + filesz;
+}
+
+if (currentOld < data.Length)
+ ms.Write(data, currentOld, data.Length - currentOld);
- if (System.IO.File.Exists(FilePath))
+byte[] newData = ms.ToArray();
+
+// ── Offset translation helper ───────────────────────────────────────
+System.Func translate = (ulong old) =>
+{
+ long d = 0;
+ foreach (var t in deltas)
+ {
+ if (old >= t.Item1) d = t.Item2;
+ else break;
+ }
+ return (ulong)((long)old + d);
+};
+
+// ── Patch ELF header: e_shoff ───────────────────────────────────────
+ulong newShoff = translate(e_shoff);
+WU64(newData, 40, newShoff);
+
+// ── Patch program headers ───────────────────────────────────────────
+for (int i = 0; i < e_phnum; i++)
+{
+ int oldHdr = (int)e_phoff + i * e_phentsize;
+ int newHdr = (int)translate((ulong)oldHdr);
+
+ WU64(newData, newHdr + 8, translate(RU64(data, oldHdr + 8))); // p_offset
+
+ if (RU32(data, oldHdr) == PT_LOAD)
+ WU64(newData, newHdr + 48, (ulong)pageSize); // p_align
+}
+
+// ── Patch section headers ───────────────────────────────────────────
+for (int i = 0; i < e_shnum; i++)
+{
+ int oldSh = (int)e_shoff + i * e_shentsize;
+ int newSh = (int)newShoff + i * e_shentsize;
+ WU64(newData, newSh + 24, translate(RU64(data, oldSh + 24))); // sh_offset
+}
+
+// ── Write patched file (with lock to handle parallel builds) ────────
+string lockFile = FilePath + ".patch-lock";
+System.IO.FileStream lockStream = null;
+try
+{
+ // Acquire an exclusive lock file to prevent concurrent writes.
+ // Retry up to 30 times with 1-second delays (handles parallel MSBuild nodes).
+ for (int attempt = 0; attempt < 30; attempt++)
+ {
+ try
+ {
+ lockStream = new System.IO.FileStream(lockFile,
+ System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite,
+ System.IO.FileShare.None);
+ break;
+ }
+ catch (System.IO.IOException)
{
- string lockFile = FilePath + ".patch-lock";
- System.IO.FileStream lockStream = null;
+ System.Threading.Thread.Sleep(1000);
+ }
+ }
- try
- {
- for (int attempt = 0; attempt < 120; attempt++)
- {
- try { lockStream = new System.IO.FileStream(lockFile, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite, System.IO.FileShare.None); break; }
- catch (System.IO.IOException) { System.Threading.Thread.Sleep(1000); }
- }
-
- if (lockStream != null && System.IO.File.Exists(FilePath))
- {
- byte[] data = null;
- for (int attempt = 0; attempt < 10; attempt++)
- {
- try { data = System.IO.File.ReadAllBytes(FilePath); break; }
- catch (System.IO.IOException) { System.Threading.Thread.Sleep(1000); }
- }
-
- if (data != null && data.Length >= 64 && data[0] == 0x7F && data[1] == (byte)'E' && data[2] == (byte)'L' && data[3] == (byte)'F' && data[4] == 2 && data[5] == 1)
- {
- System.Func RU64 = (b, o) => (ulong)b[o] | ((ulong)b[o+1] << 8) | ((ulong)b[o+2] << 16) | ((ulong)b[o+3] << 24) | ((ulong)b[o+4] << 32) | ((ulong)b[o+5] << 40) | ((ulong)b[o+6] << 48) | ((ulong)b[o+7] << 56);
- System.Func RU32 = (b, o) => (uint)b[o] | ((uint)b[o+1] << 8) | ((uint)b[o+2] << 16) | ((uint)b[o+3] << 24);
- System.Action WU64 = (b, o, v) => { b[o]=(byte)v; b[o+1]=(byte)(v>>8); b[o+2]=(byte)(v>>16); b[o+3]=(byte)(v>>24); b[o+4]=(byte)(v>>32); b[o+5]=(byte)(v>>40); b[o+6]=(byte)(v>>48); b[o+7]=(byte)(v>>56); };
-
- ulong e_phoff = RU64(data, 32);
- int e_phentsize = (int)(data[54] | (data[55] << 8));
- int e_phnum = (int)(data[56] | (data[57] << 8));
- ulong e_shoff = RU64(data, 40);
- int e_shentsize = (int)(data[58] | (data[59] << 8));
- int e_shnum = (int)(data[60] | (data[61] << 8));
-
- const uint PT_LOAD = 1;
- bool needsPatch = false;
- for (int i = 0; i < e_phnum; i++)
- {
- int hdr = (int)e_phoff + i * e_phentsize;
- if (RU32(data, hdr) == PT_LOAD && RU64(data, hdr + 48) < (ulong)pageSize) { needsPatch = true; break; }
- }
-
- if (needsPatch)
- {
- var segInfos = new System.Collections.Generic.List>();
- var loadIndices = new System.Collections.Generic.List();
- for (int i = 0; i < e_phnum; i++)
- {
- int hdr = (int)e_phoff + i * e_phentsize;
- if (RU32(data, hdr) == PT_LOAD)
- {
- loadIndices.Add(i);
- segInfos.Add(System.Tuple.Create(i, RU64(data, hdr + 8), RU64(data, hdr + 32), RU64(data, hdr + 16)));
- }
- }
- segInfos.Sort((a, b) => a.Item2.CompareTo(b.Item2));
-
- var ms = new System.IO.MemoryStream(data.Length + pageSize * loadIndices.Count);
- int currentOld = 0;
- var deltas = new System.Collections.Generic.List>();
-
- foreach (var seg in segInfos)
- {
- int oldOff = (int)seg.Item2;
- ulong vaddr = seg.Item4;
- int filesz = (int)seg.Item3;
- if (oldOff > currentOld) ms.Write(data, currentOld, oldOff - currentOld);
- long newOff = ms.Position;
- ulong requiredMod = vaddr % (ulong)pageSize;
- ulong currentMod = (ulong)newOff % (ulong)pageSize;
- if (currentMod != requiredMod)
- {
- long pad = (requiredMod >= currentMod) ? (long)(requiredMod - currentMod) : (long)((ulong)pageSize - currentMod + requiredMod);
- for (long p = 0; p < pad; p++) ms.WriteByte(0);
- newOff = ms.Position;
- }
- deltas.Add(System.Tuple.Create((ulong)oldOff, newOff - oldOff));
- ms.Write(data, oldOff, filesz);
- currentOld = oldOff + filesz;
- }
- if (currentOld < data.Length) ms.Write(data, currentOld, data.Length - currentOld);
- byte[] newData = ms.ToArray();
-
- System.Func translate = (ulong old) => { long d = 0; foreach (var t in deltas) { if (old >= t.Item1) d = t.Item2; else break; } return (ulong)((long)old + d); };
-
- ulong newShoff = translate(e_shoff);
- WU64(newData, 40, newShoff);
-
- for (int i = 0; i < e_phnum; i++)
- {
- int oldHdr = (int)e_phoff + i * e_phentsize;
- int newHdr = (int)translate((ulong)oldHdr);
- WU64(newData, newHdr + 8, translate(RU64(data, oldHdr + 8)));
- if (RU32(data, oldHdr) == PT_LOAD) WU64(newData, newHdr + 48, (ulong)pageSize);
- }
-
- for (int i = 0; i < e_shnum; i++)
- {
- int oldSh = (int)e_shoff + i * e_shentsize;
- int newSh = (int)newShoff + i * e_shentsize;
- WU64(newData, newSh + 24, translate(RU64(data, oldSh + 24)));
- }
-
- for (int attempt = 0; attempt < 20; attempt++)
- {
- try { System.IO.File.WriteAllBytes(FilePath, newData); WasPatched = true; break; }
- catch (System.IO.IOException) { System.Threading.Thread.Sleep(1000); }
- }
- if (WasPatched) Log.LogMessage(MessageImportance.High, "PatchElfPageSize: successfully patched {0}", System.IO.Path.GetFileName(FilePath));
- }
- }
- }
- }
- finally
+ if (lockStream == null)
+ {
+ Log.LogMessage(MessageImportance.High,
+ "PatchElfPageSize: could not acquire lock for {0}, skipping (another build node may be patching it)",
+ System.IO.Path.GetFileName(FilePath));
+ return true;
+ }
+
+ // Re-read and re-check: another node may have already patched while we waited.
+ byte[] freshData = System.IO.File.ReadAllBytes(FilePath);
+ bool stillNeeds = false;
+ if (freshData.Length >= 64 && freshData[0] == 0x7F && freshData[4] == 2 && freshData[5] == 1)
+ {
+ ulong freshPhoff = RU64(freshData, 32);
+ int freshPhentsz = (int)(freshData[54] | (freshData[55] << 8));
+ int freshPhnum = (int)(freshData[56] | (freshData[57] << 8));
+ for (int i = 0; i < freshPhnum; i++)
+ {
+ int h = (int)freshPhoff + i * freshPhentsz;
+ if (RU32(freshData, h) == PT_LOAD && RU64(freshData, h + 48) < (ulong)pageSize)
{
- if (lockStream != null) { lockStream.Dispose(); try { System.IO.File.Delete(lockFile); } catch { } }
+ stillNeeds = true;
+ break;
}
}
- ]]>
+ }
+
+ if (!stillNeeds)
+ {
+ Log.LogMessage(MessageImportance.Low,
+ "PatchElfPageSize: {0} was already patched by another build node",
+ System.IO.Path.GetFileName(FilePath));
+ return true;
+ }
+
+ // Even with a lock file, the actual .so may be locked for reading by another MSBuild process.
+ // Retry writing the actual file.
+ for (int attempt = 0; attempt < 30; attempt++)
+ {
+ try
+ {
+ System.IO.File.WriteAllBytes(FilePath, newData);
+ WasPatched = true;
+ break;
+ }
+ catch (System.IO.IOException)
+ {
+ if (attempt == 29) throw;
+ System.Threading.Thread.Sleep(1000);
+ }
+ }
+ WasPatched = true;
+ Log.LogMessage(MessageImportance.High,
+ "PatchElfPageSize: patched {0} (align 0x1000 -> 0x{1:X}, +{2} bytes)",
+ System.IO.Path.GetFileName(FilePath), pageSize, newData.Length - data.Length);
+}
+finally
+{
+ if (lockStream != null)
+ {
+ lockStream.Dispose();
+ try { System.IO.File.Delete(lockFile); } catch { }
+ }
+}
+]]>
+
+
<_NuGetNativeLibs Include="$(NuGetPackageRoot)/**/runtimes/android-*/native/*.so" />
diff --git a/final_fix.py b/final_fix.py
deleted file mode 100644
index 0d42a229bf25..000000000000
--- a/final_fix.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import os
-
-def fix_loc():
- path = 'osu.Game/Localisation/GraphicsSettingsStrings.cs'
- with open(path, 'r') as f:
- content = f.read()
-
- # Correct insertion before Resolution
- insertion = '\n /// \n /// "Refresh rate"\n /// \n public static LocalisableString RefreshRate => new TranslatableString(getKey(@"refresh_rate"), @"Refresh rate");\n'
-
- # We use replace with exact match to ensure indentation is correct (8 spaces)
- old_text = ' public static LocalisableString ScreenMode => new TranslatableString(getKey(@"screen_mode"), @"Screen mode");'
- new_text = old_text + insertion
-
- if old_text in content and 'RefreshRate' not in content:
- with open(path, 'w') as f:
- f.write(content.replace(old_text, new_text))
- print("Fixed Localisation")
-
-def fix_results():
- path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs'
- with open(path, 'r') as f:
- content = f.read()
-
- # Fix the if-null patterns using regex to preserve indentation exactly
- import re
-
- # if (x != null) x.Looping = false; -> x?.Looping = false;
- content = re.sub(r'if \((playerScoreTickChannel|opponentScoreTickChannel) != null\) \1\.Looping = false;', r'\1?.Looping = false;', content)
-
- # if (x != null && condition) -> if (condition) \n x?.Looping = false;
- # Wait, the original was:
- # if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent)
- # playerScoreTickChannel.Looping = false;
-
- content = re.sub(r'if \((playerScoreTickChannel|opponentScoreTickChannel) != null && (.*?)\)\s+(.*?)\.Looping = false;',
- r'if (\2)\n \1?.Looping = false;', content)
-
- with open(path, 'w') as f:
- f.write(content)
- print("Fixed ResultsScreen")
-
-fix_loc()
-fix_results()
diff --git a/fix_final.py b/fix_final.py
deleted file mode 100644
index f8044958d866..000000000000
--- a/fix_final.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import os
-
-def fix_loc():
- path = 'osu.Game/Localisation/GraphicsSettingsStrings.cs'
- with open(path, 'r') as f:
- lines = f.readlines()
-
- new_lines = []
- seen = set()
- for line in lines:
- if 'public static LocalisableString RefreshRate' in line:
- if 'RefreshRate' in seen:
- continue
- seen.add('RefreshRate')
- new_lines.append(line)
-
- with open(path, 'w') as f:
- f.writelines(new_lines)
-
-def fix_results():
- path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs'
- with open(path, 'r') as f:
- lines = f.readlines()
-
- new_lines = []
- for line in lines:
- # Simplify null checks
- if 'if (playerScoreTickChannel != null) playerScoreTickChannel.Looping = false;' in line:
- new_lines.append(line.replace('if (playerScoreTickChannel != null) playerScoreTickChannel.Looping = false;', 'playerScoreTickChannel?.Looping = false;'))
- elif 'if (opponentScoreTickChannel != null) opponentScoreTickChannel.Looping = false;' in line:
- new_lines.append(line.replace('if (opponentScoreTickChannel != null) opponentScoreTickChannel.Looping = false;', 'opponentScoreTickChannel?.Looping = false;'))
- elif 'if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent)' in line:
- new_lines.append(line.replace('if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent)', 'if (playerScoreBar.Height >= playerScorePercent)'))
- new_lines.append(line.split('if')[0] + ' playerScoreTickChannel?.Looping = false;\n')
- elif 'if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent)' in line:
- new_lines.append(line.replace('if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent)', 'if (opponentScoreBar.Height >= opponentScorePercent)'))
- new_lines.append(line.split('if')[0] + ' opponentScoreTickChannel?.Looping = false;\n')
- elif 'playerScoreTickChannel.Looping = false;' in line and 'if' not in line and '?' not in line:
- new_lines.append(line.replace('playerScoreTickChannel.Looping = false;', 'playerScoreTickChannel?.Looping = false;'))
- elif 'opponentScoreTickChannel.Looping = false;' in line and 'if' not in line and '?' not in line:
- new_lines.append(line.replace('opponentScoreTickChannel.Looping = false;', 'opponentScoreTickChannel?.Looping = false;'))
- else:
- new_lines.append(line)
-
- with open(path, 'w') as f:
- f.writelines(new_lines)
-
-fix_loc()
-fix_results()
diff --git a/fix_final_v4.py b/fix_final_v4.py
deleted file mode 100644
index 83691fd1d298..000000000000
--- a/fix_final_v4.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import os
-
-def fix_localisation():
- path = 'osu.Game/Localisation/GraphicsSettingsStrings.cs'
- with open(path, 'r') as f:
- lines = f.readlines()
-
- new_lines = []
- skip = False
- for i, line in enumerate(lines):
- if 'public static LocalisableString ScreenMode' in line:
- new_lines.append(line)
- new_lines.append('\n')
- new_lines.append(' /// \n')
- new_lines.append(' /// "Refresh rate"\n')
- new_lines.append(' /// \n')
- new_lines.append(' public static LocalisableString RefreshRate => new TranslatableString(getKey(@"refresh_rate"), @"Refresh rate");\n')
- new_lines.append('\n')
- skip = True
- continue
-
- if skip:
- if 'public static LocalisableString Resolution' in line:
- new_lines.append(' /// \n')
- new_lines.append(' /// "Resolution"\n')
- new_lines.append(' /// \n')
- new_lines.append(line)
- skip = False
- continue
-
- new_lines.append(line)
-
- with open(path, 'w') as f:
- f.writelines(new_lines)
-
-def fix_results():
- path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs'
- with open(path, 'r') as f:
- content = f.read()
-
- # Identify the block to replace
- import re
- # We want to replace from "// safety timeout" to the end of the scoreBarProgress block
- pattern = re.compile(r'// safety timeout to ensure scoreTicks don\'t play forever\s+Scheduler\.AddDelayed\(\(\) =>\s+\{.*?\}\s+scoreBarProgress\.BindValueChanged\(e =>\s+\{.*?\}\);\s+\}\);', re.DOTALL)
-
- # That's too complex. Let's just target the specific lines.
-
- fixed_content = re.sub(r'Scheduler\.AddDelayed\(\(\) =>\s+\{\s+playerScoreTickChannel\?\.Looping = false;\s+opponentScoreTickChannel\?\.Looping = false;\s+opponentScoreTickChannel\?\.Looping = false;\s+scoreBarProgress\.BindValueChanged',
- r'Scheduler.AddDelayed(() =>\n {\n playerScoreTickChannel?.Looping = false;\n opponentScoreTickChannel?.Looping = false;\n }, score_text_duration + 500);\n\n scoreBarProgress.BindValueChanged', content)
-
- with open(path, 'w') as f:
- f.write(fixed_content)
-
-fix_localisation()
-fix_results()
diff --git a/fix_formatting.py b/fix_formatting.py
deleted file mode 100644
index 369d5154bcc9..000000000000
--- a/fix_formatting.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import os
-
-path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs'
-with open(path, 'r') as f:
- lines = f.readlines()
-
-new_lines = []
-for line in lines:
- # Look for the lines with formatting issues
- if '.ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo)' in line:
- # Just rewrite it exactly as it was, maybe it was a weird tab/space mix?
- # Actually, let's look at the diff.
- new_lines.append(line)
- else:
- new_lines.append(line)
-
-with open(path, 'w') as f:
- f.writelines(new_lines)
diff --git a/osu.Android.props b/osu.Android.props
index e81b524ba8ce..9ad425da8662 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -5,51 +5,37 @@
apk
CJK;Mideast;Rare;West;Other;
Xamarin.Android.Net.AndroidMessageHandler
+
true
true
-
-
- $(NoWarn);XA1006
- $(MSBuildWarningsAsMessages);XA1006
-
false
-
+
-
+
true
true
true
- true
- partial
- false
-
-
-
-
-
-
+
-
+
true
-
-
-
-
- native
-
-
- native
-
-
-
diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml
index 0aff0dca2113..bb3cb2c56d07 100644
--- a/osu.Android/AndroidManifest.xml
+++ b/osu.Android/AndroidManifest.xml
@@ -1,7 +1,7 @@
-
-
+
+
diff --git a/osu.Android/AndroidNativeBridgeManager.cs b/osu.Android/AndroidNativeBridgeManager.cs
index 716c6208cc72..ce520f120ead 100644
--- a/osu.Android/AndroidNativeBridgeManager.cs
+++ b/osu.Android/AndroidNativeBridgeManager.cs
@@ -99,12 +99,7 @@ public void StopOboeBridge()
[MethodImpl(MethodImplOptions.NoInlining)]
public string GetOboeStatus()
{
- if (oboeBridge is not OboeAudioBridge bridge)
- {
- string? globalError = OboeAudioBridge.GetGlobalError();
- return globalError != null ? $"Failed: {globalError}" : "Not Created";
- }
-
+ if (oboeBridge is not OboeAudioBridge bridge) return "Not Created";
if (!bridge.IsActive) return "Failed: " + bridge.GetLastErrorMessage();
return cachedOboeStatus ??= $"{(bridge.IsAAudio ? "AAudio" : "OpenSLES")} [{(bridge.IsMMap ? "MMAP" : "Legacy")}]";
}
diff --git a/osu.Android/Native/OboeAudioBridge.cs b/osu.Android/Native/OboeAudioBridge.cs
index 5f15a40d566a..290c3408ac06 100644
--- a/osu.Android/Native/OboeAudioBridge.cs
+++ b/osu.Android/Native/OboeAudioBridge.cs
@@ -57,17 +57,6 @@ static OboeAudioBridge()
catch { return null; }
}
- public static string? GetGlobalError()
- {
- if (!native_loaded) return "Native library not loaded";
- try
- {
- IntPtr ptr = nOboeGetGlobalError();
- return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr);
- }
- catch { return "P/Invoke error"; }
- }
-
private OboeAudioBridge(IntPtr ptr) => nativePtr = ptr;
public bool Start()
@@ -200,7 +189,6 @@ public void Dispose()
[DllImport(lib_name)] private static extern byte nOboeIsMMap(IntPtr ptr);
[DllImport(lib_name)] private static extern void nOboeSetProvider(IntPtr ptr, IntPtr provider);
[DllImport(lib_name)] private static extern IntPtr nOboeGetLastErrorMessage(IntPtr ptr);
- [DllImport(lib_name)] private static extern IntPtr nOboeGetGlobalError();
[DllImport(lib_name)] internal static extern byte nSetThreadAffinity(int coreMask);
[DllImport(lib_name)] internal static extern IntPtr nADPFCreateSession(long targetDurationNanos);
[DllImport(lib_name)] internal static extern void nADPFReportActualDuration(IntPtr sessionPtr, long actualDurationNanos);
diff --git a/osu.Android/Native/oboe_bridge.cpp b/osu.Android/Native/oboe_bridge.cpp
index 3fd7447a0aa7..174f276152ec 100644
--- a/osu.Android/Native/oboe_bridge.cpp
+++ b/osu.Android/Native/oboe_bridge.cpp
@@ -12,15 +12,12 @@
#include
#include
#include
-
typedef uint8_t byte;
#define LOG_TAG "osu!native"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
-static std::string globalLastError;
-
OboeBridge::OboeBridge() {
LOGI("OboeBridge created");
}
@@ -30,10 +27,6 @@ OboeBridge::~OboeBridge() {
LOGI("OboeBridge destroyed");
}
-const char* OboeBridge::getLastErrorMessage() const {
- return lastErrorMsg_.c_str();
-}
-
bool OboeBridge::open(int32_t sampleRate) {
std::lock_guard lock(streamLock_);
requestedSampleRate_ = sampleRate;
@@ -65,19 +58,15 @@ bool OboeBridge::open(int32_t sampleRate) {
oboe::Result result = builder.openStream(stream_);
if (result != oboe::Result::OK) {
- LOGE("AAudio open failed (%s), falling back to unspecified API with conversions",
+ LOGE("AAudio open failed (%s), falling back to unspecified API",
oboe::convertToText(result));
builder.setAudioApi(oboe::AudioApi::Unspecified);
builder.setSharingMode(oboe::SharingMode::Shared);
- builder.setFormatConversionAllowed(true);
- builder.setChannelConversionAllowed(true);
result = builder.openStream(stream_);
}
if (result != oboe::Result::OK) {
- lastErrorMsg_ = oboe::convertToText(result);
- globalLastError = "Failed to open stream: " + lastErrorMsg_;
- LOGE("Failed to open Oboe stream: %s", lastErrorMsg_.c_str());
+ LOGE("Failed to open Oboe stream: %s", oboe::convertToText(result));
return false;
}
@@ -89,13 +78,16 @@ bool OboeBridge::open(int32_t sampleRate) {
// LatencyTuner will then attempt to shrink it to 1x burst if stable.
stream_->setBufferSizeInFrames(stream_->getFramesPerBurst() * 2);
+ // Initialise LatencyTuner for dynamic buffer management.
tuner_ = std::make_unique(*stream_);
- LOGI("Oboe stream opened: rate=%d, burst=%d, buffer=%d, api=%s, sharing=%s, mmap=%s",
+ LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
+ "bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s",
+ stream_->getAudioApi() == oboe::AudioApi::AAudio ? "AAudio" : "OpenSLES",
stream_->getSampleRate(),
stream_->getFramesPerBurst(),
stream_->getBufferSizeInFrames(),
- stream_->getAudioApi() == oboe::AudioApi::AAudio ? "AAudio" : "OpenSLES",
+ stream_->getBufferCapacityInFrames(),
stream_->getSharingMode() == oboe::SharingMode::Exclusive ? "Exclusive" : "Shared",
oboe::OboeExtensions::isMMapUsed(stream_.get()) ? "yes" : "no");
@@ -106,16 +98,14 @@ bool OboeBridge::start() {
std::lock_guard lock(streamLock_);
if (!stream_) {
- lastErrorMsg_ = "Stream not opened";
- LOGE("Cannot start: %s", lastErrorMsg_.c_str());
+ LOGE("Cannot start: stream not opened");
return false;
}
oboe::Result result = stream_->requestStart();
if (result != oboe::Result::OK) {
- lastErrorMsg_ = oboe::convertToText(result);
- LOGE("Failed to start Oboe stream: %s", lastErrorMsg_.c_str());
+ LOGE("Failed to start Oboe stream: %s", oboe::convertToText(result));
return false;
}
@@ -183,6 +173,7 @@ void OboeBridge::setProvider(OboeAudioProvider provider) {
oboe::DataCallbackResult OboeBridge::onAudioReady(
oboe::AudioStream* stream, void* audioData, int32_t numFrames) {
+
OboeAudioProvider provider = provider_.load(std::memory_order_acquire);
if (provider) {
@@ -200,6 +191,7 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
memset(audioData, 0, byteCount);
}
+
uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed);
if ((count & 127) == 0) {
@@ -218,11 +210,15 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
int num_cores = sysconf(_SC_NPROCESSORS_CONF);
if (num_cores > 0) {
+ // S23 Ultra (Snapdragon 8 Gen 2) layout: 1 Prime + 2 Gold + 2 Gold + 3 Silver.
+ // Indices are typically: 0-2 (Silver), 3-4 (Gold), 5-6 (Gold), 7 (Prime).
+ // We want to target the Prime (7) and Gold (3-6) cores.
if (num_cores >= 8) {
for (int i = 3; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
}
} else {
+ // Fallback for devices with fewer cores.
for (int i = num_cores / 2; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
}
@@ -306,13 +302,9 @@ void OboeBridge::updateLatency() {
extern "C" {
OSU_EXPORT intptr_t nOboeCreate(int sampleRate) {
- globalLastError = "";
auto* bridge = new (std::nothrow) OboeBridge();
- if (!bridge) {
- globalLastError = "Out of memory";
- return 0;
- }
+ if (!bridge) return 0;
if (!bridge->open(sampleRate)) {
delete bridge;
@@ -376,15 +368,6 @@ OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) {
if (bridge) bridge->setProvider(provider);
}
-OSU_EXPORT const char* nOboeGetLastErrorMessage(intptr_t ptr) {
- auto* bridge = reinterpret_cast(ptr);
- return bridge ? bridge->getLastErrorMessage() : "Invalid pointer";
-}
-
-OSU_EXPORT const char* nOboeGetGlobalError() {
- return globalLastError.empty() ? nullptr : globalLastError.c_str();
-}
-
} // extern "C"
extern "C" {
diff --git a/osu.Android/Native/oboe_bridge.h b/osu.Android/Native/oboe_bridge.h
index 85b3227ea1e2..4b8f08222ecf 100644
--- a/osu.Android/Native/oboe_bridge.h
+++ b/osu.Android/Native/oboe_bridge.h
@@ -10,7 +10,6 @@
#include
#include
#include
-#include
/// Callback function type for providing PCM audio data to the Oboe stream.
/// Returns the number of frames actually written to the buffer.
@@ -34,7 +33,6 @@ class OboeBridge : public oboe::AudioStreamCallback {
bool isAAudio() const;
bool isMMap() const;
void setProvider(OboeAudioProvider provider);
- const char* getLastErrorMessage() const;
// oboe::AudioStreamCallback
oboe::DataCallbackResult onAudioReady(
@@ -55,7 +53,6 @@ class OboeBridge : public oboe::AudioStreamCallback {
std::atomic provider_{nullptr};
std::atomic affinitySet_{false};
int32_t requestedSampleRate_{0};
- std::string lastErrorMsg_;
void updateLatency();
bool reopenAndRestart();
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 2a18de49b934..0e6b03cd6ebc 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -47,10 +47,6 @@ namespace osu.Android
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity, ISurfaceHolderCallback
{
- protected OsuGameActivity(IntPtr handle, JniHandleOwnership transfer) : base()
- {
- }
-
private static readonly string[] osu_url_schemes = { "osu", "osump" };
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
diff --git a/osu.Game.OsuGameBase.patch b/osu.Game.OsuGameBase.patch
deleted file mode 100644
index b72a5e28b674..000000000000
--- a/osu.Game.OsuGameBase.patch
+++ /dev/null
@@ -1,15 +0,0 @@
---- a/osu.Game/OsuGameBase.cs
-+++ b/osu.Game/OsuGameBase.cs
-@@ -122,6 +122,12 @@
-
- public virtual bool IsVulkanSupported => false;
-
-+ public virtual bool IsOboeActive => false;
-+
-+ public virtual string OboeStatus => string.Empty;
-+
-+ public virtual double OboeLatency => -1;
-+
- public virtual string Version
- {
- get
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
index b7755dd48e3f..16be596df96b 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
index 1671c8cb1adc..c308a3d4a3e4 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
index d12fb104f88a..93b0d73e85c8 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index b97e0b93ca04..0bd0b782595d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -5,13 +5,16 @@
using System.Collections.Generic;
using System;
+
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Utils;
+
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
+
using JetBrains.Annotations;
using osuTK.Graphics;
using osuTK;
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
index 0b1feae5d8e4..f8213d7936e5 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Game.Tests.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml
index f9fa3d105472..48fa69445873 100644
--- a/osu.Game.Tests.Android/AndroidManifest.xml
+++ b/osu.Game.Tests.Android/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs
index bf88cee11bae..da10c15cd752 100644
--- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs
@@ -52,6 +52,7 @@ public void TestDailyChallenge()
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
],
+ StartDate = DateTimeOffset.Now.AddSeconds(-10),
EndDate = DateTimeOffset.Now.AddHours(12),
Category = RoomCategory.DailyChallenge
};
@@ -74,6 +75,7 @@ public void TestUseTheseModsUnavailableIfNoFreeMods()
AllowedMods = []
}
],
+ StartDate = DateTimeOffset.Now.AddSeconds(-10),
EndDate = DateTimeOffset.Now.AddHours(12),
Category = RoomCategory.DailyChallenge
};
@@ -106,18 +108,19 @@ public void TestNotifications()
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
],
+ StartDate = DateTimeOffset.Now.AddSeconds(-10),
EndDate = DateTimeOffset.Now.AddHours(12),
Category = RoomCategory.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
- AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
+ AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = (room.RoomID ?? 0) });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
- AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
+ AddUntilStep("notification posted", () => notificationOverlay.AllNotifications.OfType().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
}
[Test]
@@ -134,12 +137,13 @@ public void TestConclusionNotificationDoesNotFireOnDisconnect()
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
],
+ StartDate = DateTimeOffset.Now.AddSeconds(-10),
EndDate = DateTimeOffset.Now.AddHours(12),
Category = RoomCategory.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
- AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
+ AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = (room.RoomID ?? 0) });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs
index eda596effb1e..e1986fa77bbd 100644
--- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs
@@ -78,8 +78,7 @@ public void TestBasicAppearance()
CoverUrl = TestResources.COVER_IMAGE_3,
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
- var testScore = TestResources.CreateTestScoreInfo();
- testScore.TotalScore = RNG.Next(1_000_000);
+
feed.AddNewScore(ev);
}, 50);
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
index 97b957df4336..c715105e01bf 100644
--- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs
@@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
@@ -17,7 +19,6 @@
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
-using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
namespace osu.Game.Tests.Visual.DailyChallenge
{
@@ -29,7 +30,7 @@ public partial class TestSceneDailyChallengeIntro : OnlinePlayTestScene
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
- private Room room = null!;
+ private Room? room;
[BackgroundDependencyLoader]
private void load()
@@ -44,31 +45,45 @@ private void load()
[Test]
public void TestDailyChallenge()
{
- startChallenge();
- AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
+ startChallenge("first");
+ AddUntilStep("wait for button room", () => this.ChildrenOfType().FirstOrDefault()?.Room?.RoomID == room?.RoomID);
+ AddStep("push screen", () =>
+ {
+ if (room != null)
+ LoadScreen(new DailyChallengeIntro(room));
+ });
}
[Test]
public void TestPlayIntroOnceFlag()
{
- startChallenge();
+ startChallenge("first");
+ AddUntilStep("wait for first button room", () => this.ChildrenOfType().FirstOrDefault()?.Room?.RoomID == room?.RoomID);
+
AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true));
+ AddAssert("intro played flag is true", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed));
- startChallenge();
+ startChallenge("second");
- AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False);
+ AddUntilStep("wait for button to update to second room", () => this.ChildrenOfType().FirstOrDefault()?.Room?.RoomID == room?.RoomID);
+ AddUntilStep("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed));
- AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
- AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True);
+ AddStep("push screen", () =>
+ {
+ if (room != null)
+ LoadScreen(new DailyChallengeIntro(room));
+ });
}
- private void startChallenge()
+ private void startChallenge(string suffix)
{
- AddStep("add room", () =>
+ AddStep($"reset info ({suffix})", () => metadataClient.DailyChallengeUpdated(null!));
+ AddStep($"reset room ({suffix})", () => room = null);
+ AddStep($"add room ({suffix})", () =>
{
- API.Perform(new CreateRoomRequest(room = new Room
+ var newRoom = new Room
{
- Name = "Daily Challenge: June 4, 2024",
+ Name = $"Daily Challenge {suffix}",
Playlist =
[
new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo))
@@ -77,12 +92,20 @@ private void startChallenge()
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
],
- StartDate = DateTimeOffset.Now,
+ StartDate = DateTimeOffset.Now.AddSeconds(-10),
EndDate = DateTimeOffset.Now.AddHours(24),
Category = RoomCategory.DailyChallenge
- }));
+ };
+ room = newRoom;
+ API.Perform(new CreateRoomRequest(newRoom));
+ });
+ AddUntilStep($"wait for room id ({suffix})", () => room?.RoomID is > 0);
+ AddUntilStep($"wait for playlist id ({suffix})", () => room?.Playlist.All(p => p.ID > 0) is true);
+ AddStep($"signal client ({suffix})", () =>
+ {
+ if (room?.RoomID is long roomId)
+ metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId });
});
- AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
}
}
}
diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs
index 4619fad93898..8be82f50a98b 100644
--- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs
+++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs
@@ -76,8 +76,6 @@ public void TestBasicAppearance()
CoverUrl = TestResources.COVER_IMAGE_3,
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
- var testScore = TestResources.CreateTestScoreInfo();
- testScore.TotalScore = RNG.Next(1_000_000);
totals.AddNewScore(ev);
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
index 090a5e71242b..44c2e7eb55fd 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
@@ -120,7 +120,7 @@ public void TestPlaylistItemSelectedOnCreate()
];
});
- AddAssert("first playlist item selected", () => room.Playlist.Count > 0 && match.SelectedItem.Value == room.Playlist[0]);
+ AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
index 7313679bc809..a3f6fa6671df 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
@@ -165,7 +165,7 @@ public void TestBeatmapAndRuleset_FollowSelection()
AddStep("load screen", () => LoadScreen(new TestPlaylistsScreen(screen = new TestPlaylistsRoomSubScreen(room))));
AddUntilStep("wait for load", () => screen.IsLoaded);
- AddStep("select first item", () => screen.SelectedItem.Value = room.Playlist.FirstOrDefault());
+ AddStep("select first item", () => screen.SelectedItem.Value = room.Playlist[0]);
AddUntilStep("first beatmap selected", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]));
AddUntilStep("osu ruleset selected", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
@@ -652,7 +652,7 @@ public void TestUserStyle_Reset_OnFreestyleDisabled()
AddUntilStep("user style reset", () => screen.UserBeatmap.Value == null && screen.UserRuleset.Value == null);
AddUntilStep("beatmap/ruleset set", () => Beatmap.Value.BeatmapInfo.Equals(importedSet.Beatmaps[0]) && Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
- AddStep("select first playlist item", () => screen.SelectedItem.Value = room.Playlist.FirstOrDefault());
+ AddStep("select first playlist item", () => screen.SelectedItem.Value = room.Playlist[0]);
// Set mods (DT+HR), validate by selecting second playlist item where only DT is allowed.
AddStep("set user mods style", () => screen.UserMods.Value = [new OsuModDoubleTime(), new OsuModHardRock()]);
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 8bd6d28daa56..462bed398745 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -288,7 +288,7 @@ private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, Can
// Populate users.
await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
if (joinedRoom.Host != null)
- await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false);
+ await PopulateUsers(new[] { joinedRoom.Host }).ConfigureAwait(false);
// Update the stored room (must be done on update thread for thread-safety).
await runOnUpdateThreadAsync(() =>
@@ -306,12 +306,12 @@ await runOnUpdateThreadAsync(() =>
APIRoom.ChannelId = joinedRoom.ChannelID;
APIRoom.Host = joinedRoom.Host?.User;
APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
- APIRoom.CurrentPlaylistItem = APIRoom.Playlist.FirstOrDefault(item => item.ID == joinedRoom.Settings.PlaylistItemId);
+ APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
APIRoom.EndDate = null;
- Debug.Assert(LocalUser != null);
- addUserToAPIRoom(LocalUser);
+ var localUser = LocalUser;
+ if (localUser != null) addUserToAPIRoom(localUser);
foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State);
@@ -1015,7 +1015,7 @@ private void updateLocalRoomSettings(MultiplayerRoomSettings settings)
APIRoom.Type = Room.Settings.MatchType;
APIRoom.QueueMode = Room.Settings.QueueMode;
APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration;
- APIRoom.CurrentPlaylistItem = APIRoom.Playlist.FirstOrDefault(item => item.ID == settings.PlaylistItemId);
+ APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
APIRoom.AutoSkip = Room.Settings.AutoSkip;
SettingsChanged?.Invoke(settings);
diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs
index be22fc3c3085..480572f152ac 100644
--- a/osu.Game/Screens/Menu/DailyChallengeButton.cs
+++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs
@@ -149,7 +149,9 @@ private void dailyChallengeChanged(ValueChangedEvent _)
}
else
{
- var roomRequest = new GetRoomRequest(info.Value.Value.RoomID);
+ if (info.Value is not DailyChallengeInfo infoValue) return;
+
+ var roomRequest = new GetRoomRequest(infoValue.RoomID);
roomRequest.Success += room =>
{
@@ -164,13 +166,14 @@ private void dailyChallengeChanged(ValueChangedEvent _)
statics.SetValue(Static.DailyChallengeIntroPlayed, false);
// we only want to notify the user if the new challenge just went live.
- if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value).TotalSeconds) < 1800)
+ if (room.StartDate != null && Math.Abs((DateTimeOffset.Now - (room.StartDate ?? DateTimeOffset.Now)).TotalSeconds) < 1800)
notificationOverlay?.Post(new NewDailyChallengeNotification(room));
}
updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true);
};
+
api.Queue(roomRequest);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
index 15eefc10b69c..2ee2cd35b7d8 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
{
private readonly Room room;
- private readonly PlaylistItem playlistItem;
+ private readonly PlaylistItem? playlistItem;
///
/// Any mods applied by/to the local user.
@@ -70,7 +70,6 @@ public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePres
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
- [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
private readonly DailyChallengeBeatmapAvailabilityTracker beatmapAvailabilityTracker;
[Resolved]
@@ -113,10 +112,17 @@ public DailyChallenge(Room room)
{
this.room = room;
- playlistItem = room.Playlist.Single();
+ playlistItem = room.Playlist.FirstOrDefault();
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
- beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(playlistItem);
+ beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(playlistItem ?? new PlaylistItem(new BeatmapInfo()));
+ }
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+ dependencies.CacheAs(beatmapAvailabilityTracker);
+ return dependencies;
}
[BackgroundDependencyLoader]
@@ -132,7 +138,7 @@ private void load(AudioManager audio)
Children = new Drawable[]
{
beatmapAvailabilityTracker,
- new ScreenStack(new RoomBackgroundScreen(playlistItem))
+ new ScreenStack(new RoomBackgroundScreen(playlistItem ?? new PlaylistItem(new BeatmapInfo())))
{
RelativeSizeAxes = Axes.Both,
},
@@ -160,7 +166,7 @@ private void load(AudioManager audio)
{
new Drawable[]
{
- new DrawableRoomPlaylistItem(playlistItem, true)
+ playlistItem == null ? new Container() : new DrawableRoomPlaylistItem(playlistItem, true)
{
RelativeSizeAxes = Axes.X,
AllowReordering = false,
@@ -236,7 +242,7 @@ private void load(AudioManager audio)
},
null,
// Middle column (leaderboard)
- leaderboard = new DailyChallengeLeaderboard(room, playlistItem)
+ leaderboard = new DailyChallengeLeaderboard(room, playlistItem ?? new PlaylistItem(new BeatmapInfo()))
{
RelativeSizeAxes = Axes.Both,
PresentScore = presentScore,
@@ -318,7 +324,11 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
IsValidMod = _ => false
});
- if (playlistItem.AllowedMods.Any())
+ var item = playlistItem;
+
+ if (item == null) return;
+
+ if (item.AllowedMods.Any())
{
footerButtons.Insert(-1, new UserModSelectButton
{
@@ -330,8 +340,8 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
Action = () => userModsSelectOverlay.Show(),
});
- var rulesetInstance = rulesets.GetRuleset(playlistItem.RulesetID)!.CreateInstance();
- var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance));
+ var rulesetInstance = rulesets.GetRuleset(item.RulesetID)!.CreateInstance();
+ var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance));
userModsSelectOverlay.IsValidMod = leaderboard.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
}
@@ -343,13 +353,14 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
private void presentScore(long id)
{
- if (this.IsCurrentScreen())
- this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem));
+ if (this.IsCurrentScreen() && playlistItem != null)
+ this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID ?? 0, playlistItem));
}
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)
{
- if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItem.ID)
+ var playlistItemLocal = playlistItem;
+ if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItemLocal?.ID)
return;
userLookupCache.GetUserAsync(e.UserID).ContinueWith(t =>
@@ -427,7 +438,7 @@ public override void OnEntering(ScreenTransitionEvent e)
API.Queue(new JoinRoomRequest(room, null));
startLoopingTrack(this, musicController);
- metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t =>
+ metadataClient.BeginWatchingMultiplayerRoom((room.RoomID ?? 0)).ContinueWith(t =>
{
if (t.Exception != null)
{
@@ -436,7 +447,8 @@ public override void OnEntering(ScreenTransitionEvent e)
}
MultiplayerPlaylistItemStats[] stats = t.GetResultSafely();
- var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID);
+ var playlistItemLocal = playlistItem;
+ var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItemLocal?.ID);
if (itemStats == null) return;
@@ -479,14 +491,14 @@ public override bool OnExiting(ScreenExitEvent e)
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
API.Queue(new PartRoomRequest(room));
- metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget();
+ metadataClient.EndWatchingMultiplayerRoom((room.RoomID ?? 0)).FireAndForget();
return base.OnExiting(e);
}
- public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem item)
+ public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem? item)
{
- if (!screen.IsCurrentScreen())
+ if (item == null || !screen.IsCurrentScreen())
return;
var beatmap = beatmaps.QueryOnlineBeatmapId(item.Beatmap.OnlineID);
@@ -520,7 +532,7 @@ private void cancelTrackLooping()
private void updateMods()
{
- if (!this.IsCurrentScreen())
+ if (!this.IsCurrentScreen() || playlistItem == null)
return;
Mods.Value = userMods.Value.Concat(playlistItem.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList();
@@ -529,6 +541,10 @@ private void updateMods()
private void startPlay()
{
sampleStart?.Play();
+
+ if (playlistItem == null)
+ return;
+
this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem)
{
Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores())
@@ -547,12 +563,12 @@ protected override void Dispose(bool isDisposing)
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
- if (!this.IsCurrentScreen())
+ if (!this.IsCurrentScreen() || playlistItem == null)
return;
// We can only handle the current daily challenge beatmap.
// If the import was for a different beatmap, pass the duty off to global handling.
- if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID)
+ if (playlistItem.Beatmap.BeatmapSet != null && beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet.OnlineID)
{
this.Exit();
game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID);
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs
index bda1135c879c..89c4c0255b35 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs
@@ -60,7 +60,6 @@ public override void Add(Drawable drawable)
public override bool Remove(Drawable drawable, bool disposeImmediately)
{
int index = content.IndexOf(drawable);
-
if (index >= 0)
navigationFlow.Remove(navigationFlow[index], true);
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs
index 075d2af0aa83..00fb5c201c1b 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs
@@ -35,7 +35,7 @@ public partial class DailyChallengeIntro : OsuScreen
public override bool? ApplyModTrackAdjustments => true;
private readonly Room room;
- private readonly PlaylistItem item;
+ private readonly PlaylistItem? item;
private Container introContent = null!;
private Container topTitleDisplay = null!;
@@ -56,7 +56,6 @@ public partial class DailyChallengeIntro : OsuScreen
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
- [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
private readonly DailyChallengeBeatmapAvailabilityTracker beatmapAvailabilityTracker;
private bool shouldBePlayingMusic;
@@ -88,15 +87,22 @@ public partial class DailyChallengeIntro : OsuScreen
public DailyChallengeIntro(Room room)
{
this.room = room;
- item = room.Playlist.Single();
+ item = room.Playlist.FirstOrDefault();
ValidForResume = false;
- beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(item);
+ beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(item ?? new PlaylistItem(new BeatmapInfo()));
}
protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider);
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+ dependencies.CacheAs(beatmapAvailabilityTracker);
+ return dependencies;
+ }
+
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio)
{
@@ -104,6 +110,8 @@ private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache,
StarRatingDisplay starRatingDisplay;
+ if (item == null) return;
+
IBeatmapInfo beatmap = item.Beatmap;
Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)!.CreateInstance();
@@ -352,7 +360,7 @@ public override void OnEntering(ScreenTransitionEvent e)
beatmapAvailabilityTracker.Availability.BindValueChanged(availability =>
{
- if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable)
+ if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable && item != null)
DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item);
}, true);
@@ -449,8 +457,11 @@ private void beginAnimation()
Schedule(() =>
{
shouldBePlayingMusic = true;
- DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item);
- ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item);
+ if (item != null)
+ DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item);
+
+ if (item != null)
+ ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item);
playBeatmapImpactSample();
});
}
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs
index 62c5c0c8dfb5..2cfc5aaaedd7 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs
@@ -138,7 +138,7 @@ public void RefetchScores()
if (request?.CompletionState == APIRequestCompletionState.Waiting)
return;
- request = new IndexPlaylistScoresRequest(room.RoomID!.Value, playlistItem.ID);
+ request = new IndexPlaylistScoresRequest((room.RoomID ?? 0), playlistItem.ID);
request.Success += req => Schedule(() =>
{
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs
index bf01ee6b522d..0b31dbd52762 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs
@@ -134,8 +134,8 @@ private void updateState()
return;
}
- var roomDuration = room.EndDate.Value - room.StartDate.Value;
- var remaining = room.EndDate.Value - DateTimeOffset.Now;
+ var roomDuration = (room.EndDate ?? DateTimeOffset.Now) - (room.StartDate ?? DateTimeOffset.Now);
+ var remaining = (room.EndDate ?? DateTimeOffset.Now) - DateTimeOffset.Now;
timeText.Text = remaining.ToString(@"hh\:mm\:ss");
progress.Progress = remaining.TotalSeconds / roomDuration.TotalSeconds;
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs
index 9ca650c0444e..25f1c7465ca3 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs
@@ -1,10 +1,9 @@
// 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.Screens;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
+using osu.Framework.Screens;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Localisation;
@@ -19,6 +18,8 @@ public partial class NewDailyChallengeNotification : SimpleNotification
{
private readonly Room room;
+ private BeatmapCardNano card = null!;
+
public NewDailyChallengeNotification(Room room)
{
this.room = room;
@@ -28,18 +29,9 @@ public NewDailyChallengeNotification(Room room)
private void load(OsuGame? game, SessionStatics statics)
{
Text = DailyChallengeStrings.ChallengeLiveNotification;
-
- var item = room.Playlist.FirstOrDefault();
-
- if (item?.Beatmap.BeatmapSet is APIBeatmapSet beatmapSet)
- {
- Content.Add(new BeatmapCardNano(beatmapSet)
- {
- RelativeSizeAxes = Axes.X,
- Width = 1,
- });
- }
-
+ var playlistItem = room.Playlist.FirstOrDefault();
+ if (playlistItem != null)
+ Content.Add(card = new BeatmapCardNano((APIBeatmapSet)playlistItem.Beatmap.BeatmapSet!));
Activated = () =>
{
if (statics.Get(Static.DailyChallengeIntroPlayed))
@@ -50,5 +42,11 @@ private void load(OsuGame? game, SessionStatics statics)
return true;
};
}
+
+ protected override void Update()
+ {
+ base.Update();
+ card.Width = Content.DrawWidth;
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs
index 48c64f2f6674..6882ddabe858 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs
@@ -53,9 +53,9 @@ private void load(AudioManager audio)
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
- public bool AddUser(APIUser user)
+ public bool AddUser(APIUser? user)
{
- if (avatars.Any(a => a.User.Id == user.Id))
+ if (user == null || avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs
index e2d5fa7890bf..55418a2eab46 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs
@@ -92,7 +92,7 @@ private void load(OsuColour colours)
thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
- Size = new Vector2(MatchmakingSelectPanel.HEIGHT),
+ Size = new Vector2(HEIGHT),
Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS },
Children = new Drawable[]
{
@@ -114,8 +114,8 @@ private void load(OsuColour colours)
},
buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
- X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS,
- Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS,
+ X = HEIGHT - BeatmapCard.CORNER_RADIUS,
+ Width = BeatmapCard.WIDTH - HEIGHT + BeatmapCard.CORNER_RADIUS,
FavouriteState = { BindTarget = favouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs
index 04d72185cde1..2f0f830c01d2 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
@@ -23,6 +24,8 @@ public partial class PlayerPanelOverlay : CompositeDrawable
[Resolved]
private MultiplayerClient client { get; set; } = null!;
+ public IReadOnlyList Panels => panels.Children;
+
private Container panels = null!;
private PlayerPanelCellContainer gridLayout = null!;
private PlayerPanelCellContainer splitLayoutLeft = null!;
@@ -111,6 +114,8 @@ public Drawable? DisplayArea
private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() =>
{
+ if (user.User == null) return;
+
panels.Add(new PlayerPanel(user)
{
Anchor = Anchor.Centre,
@@ -123,7 +128,8 @@ private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() =>
private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() =>
{
- if (panels.FirstOrDefault(p => p.RoomUser.Equals(user)) is { } panel) panel.HasQuit = true;
+ var panel = panels.FirstOrDefault(p => p.RoomUser.Equals(user));
+ panel?.HasQuit = true;
updateDisplay();
});
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs
index dc6cc06e9448..660f64b45b67 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs
@@ -11,14 +11,13 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
-using osu.Framework.Logging;
+
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
-using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -73,8 +72,16 @@ public partial class GameplayWarmupScreen : RankedPlaySubScreen
[BackgroundDependencyLoader]
private void load()
{
- APIBeatmap beatmap = beatmapLookupCache.GetBeatmapAsync(Client.Room!.CurrentPlaylistItem.BeatmapID).GetResultSafely()!;
- lastLookupResult.Value = SongSelect.BeatmapSetLookupResult.Completed(beatmap.BeatmapSet);
+ APIBeatmap? beatmap = null;
+
+ var item = Client.Room?.CurrentPlaylistItem;
+
+ if (item != null)
+ {
+ beatmap = beatmapLookupCache.GetBeatmapAsync(item.BeatmapID).GetResultSafely();
+ if (beatmap?.BeatmapSet != null)
+ lastLookupResult.Value = SongSelect.BeatmapSetLookupResult.Completed(beatmap.BeatmapSet);
+ }
var matchState = Client.Room?.MatchState as RankedPlayRoomState;
Debug.Assert(matchState != null);
@@ -134,17 +141,19 @@ private void load()
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 4f),
Direction = FillDirection.Vertical,
- Children =
- [
- new ShearAligningWrapper(new TitleWedge(beatmap))
- {
- Shear = -OsuGame.SHEAR,
- },
- new ShearAligningWrapper(new MetadataWedge(beatmap))
- {
- Shear = -OsuGame.SHEAR,
- },
- ]
+ Children = beatmap == null
+ ? System.Array.Empty()
+ :
+ [
+ new ShearAligningWrapper(new TitleWedge(beatmap))
+ {
+ Shear = -OsuGame.SHEAR,
+ },
+ new ShearAligningWrapper(new MetadataWedge(beatmap))
+ {
+ Shear = -OsuGame.SHEAR,
+ },
+ ]
}
}
}
@@ -157,7 +166,8 @@ protected override void LoadComplete()
{
base.LoadComplete();
- MultiplayerPlaylistItem item = Client.Room!.CurrentPlaylistItem;
+ var item = Client.Room?.CurrentPlaylistItem;
+ if (item == null) return;
RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!;
Ruleset rulesetInstance = ruleset.CreateInstance();
@@ -200,16 +210,11 @@ public override void OnEntering(RankedPlaySubScreen? previous)
}
}
- if (card == null)
+ card ??= new RankedPlayCard(matchInfo.LastPlayedCard)
{
- Logger.Log($"Played card {matchInfo.LastPlayedCard.Card.ID} was not on the screen.", level: LogLevel.Error);
-
- card = new RankedPlayCard(matchInfo.LastPlayedCard)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- };
- }
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
cardColumn.Add(card);
diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs
index 657fbb13808d..cba7ab4f1620 100644
--- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs
+++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs
@@ -82,23 +82,28 @@ public partial class RankedPlayMatchInfo : Component
private readonly List opponentCards = new List();
private readonly Bindable stage = new Bindable();
+ private APIUser player = null!;
+
[Resolved]
private MultiplayerClient client { get; set; } = null!;
- private APIUser player = null!;
+
protected override void LoadComplete()
{
base.LoadComplete();
- player = client.LocalUser!.User!;
+ var localUser = client.LocalUser;
+ if (localUser?.User != null) player = localUser.User;
+ else player = new APIUser { Id = localUser?.UserID ?? -1, Username = "Unknown" };
client.MatchRoomStateChanged += onMatchRoomStateChanged;
client.RankedPlayCardAdded += onCardAdded;
client.RankedPlayCardRemoved += onCardRemoved;
client.RankedPlayCardPlayed += onCardPlayed;
- var roomState = (RankedPlayRoomState)client.Room!.MatchState!;
+ if (client.Room?.MatchState is not RankedPlayRoomState roomState)
+ return;
onMatchRoomStateChanged(roomState);
diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs
index 9aa11b7f30dc..6e21536e553b 100644
--- a/osu.Game/Screens/Play/HUD/SpectatorList.cs
+++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs
@@ -46,7 +46,7 @@ public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
private SpectatorClient client { get; set; } = null!;
[Resolved(CanBeNull = true)]
- private GameplayState gameplayState { get; set; } = null!;
+ private GameplayState? gameplayState { get; set; }
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
@@ -87,7 +87,10 @@ protected override void LoadComplete()
{
base.LoadComplete();
- if (gameplayState != null) ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState);
+ var state = gameplayState;
+
+ if (state != null)
+ ((IBindable)userPlayingState).BindTo(state.PlayingState);
multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom());
diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs
index 262816ae89b7..63c7ade234e8 100644
--- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs
@@ -17,7 +17,7 @@ public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDepende
TestMultiplayerClient MultiplayerClient { get; }
///
- /// The cached .
+ /// The cached .
///
TestSpectatorClient SpectatorClient { get; }
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index de62ed25394e..4faea505b930 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -65,7 +65,7 @@ public partial class TestMultiplayerClient : MultiplayerClient
public new MultiplayerRoom Room => throw new InvalidOperationException($"Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. "
+ $"Use {nameof(ClientRoom)} if this was intended.");
- public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
+ public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.Id);
public Action? RoomSetupAction;
@@ -147,7 +147,7 @@ public void RemoveUser(APIUser user)
((IMultiplayerClient)this).UserLeft(clone(new MultiplayerRoomUser(user.Id)));
if (ServerRoom.Users.Any())
- TransferHost(ServerRoom.Users.FirstOrDefault()?.UserID ?? 0);
+ TransferHost(ServerRoom.Users.First().UserID);
}
public void ChangeRoomState(MultiplayerRoomState newState)
@@ -241,7 +241,7 @@ protected override async Task JoinRoomInternal(long roomId, str
if (password != ServerAPIRoom.Password)
throw new InvalidOperationException("Invalid password.");
- lastPlaylistItemId = ServerAPIRoom.Playlist.Any() ? ServerAPIRoom.Playlist.Max(item => item.ID) : 0;
+ lastPlaylistItemId = ServerAPIRoom.Playlist.Max(item => item.ID);
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
{
@@ -741,10 +741,7 @@ private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true)
Debug.Assert(ServerRoom != null);
// Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
- MultiplayerPlaylistItem? nextItem = upcomingItems.FirstOrDefault() ?? ServerRoom.Playlist.OrderByDescending(i => i.PlayedAt).FirstOrDefault();
-
- if (nextItem == null)
- return;
+ MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? ServerRoom.Playlist.OrderByDescending(i => i.PlayedAt).First();
currentIndex = ServerRoom.Playlist.IndexOf(nextItem);
@@ -765,7 +762,11 @@ private async Task updatePlaylistOrder(MultiplayerRoom room)
switch (room.Settings.QueueMode)
{
default:
- orderedActiveItems = ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList();
+ orderedActiveItems = ServerRoom.Playlist
+ .Where(item => !item.Expired)
+ .OrderBy(item => item.PlaylistOrder)
+ .ThenBy(item => item.ID)
+ .ToList();
break;
case QueueMode.AllPlayersRoundRobin:
@@ -779,14 +780,8 @@ private async Task updatePlaylistOrder(MultiplayerRoom room)
}
orderedActiveItems = itemsByPriority
- // Order by each user's priority.
.OrderBy(i => i.priority)
- // Many users will have the same priority of items, so attempt to break the tie by maintaining previous ordering.
- // Suppose there are two users: User1 and User2. User1 adds two items, and then User2 adds a third. If the previous order is not maintained,
- // then after playing the first item by User1, their second item will become priority=0 and jump to the front of the queue (because it was added first).
.ThenBy(i => i.item.PlaylistOrder)
- // If there are still ties (normally shouldn't happen), break ties by making items added earlier go first.
- // This could happen if e.g. the item orders get reset.
.ThenBy(i => i.item.ID)
.Select(i => i.item)
.ToList();
@@ -816,16 +811,22 @@ private T clone(T incoming)
byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
var result = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
- if (result is MultiplayerRoom room)
+ if (incoming is MultiplayerRoomUser { User: { } } sourceUser && result is MultiplayerRoomUser targetUser) targetUser.User = sourceUser.User;
+
+ if (incoming is MultiplayerRoom sourceRoom && result is MultiplayerRoom targetRoom)
{
- if (room.Host is { } host) host.User = ServerRoom!.Users.FirstOrDefault(u => u.UserID == host.UserID)?.User;
+ foreach (var user in targetRoom.Users)
+ user.User = sourceRoom.Users.FirstOrDefault(u => u.UserID == user.UserID)?.User;
- foreach (var user in room.Users) user.User = ServerRoom!.Users.FirstOrDefault(u => u.UserID == user.UserID)?.User;
+ targetRoom.Host?.User = sourceRoom.Host?.User;
+ }
+ else if (incoming is MultiplayerRoomUser sourceSingleUser && result is MultiplayerRoomUser targetSingleUser)
+ {
+ targetSingleUser.User = sourceSingleUser.User;
}
return result;
}
-
public override Task DisconnectInternal()
{
isConnected.Value = false;
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index 1f7296e0af03..95c7b8355c9c 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -3,49 +3,34 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using Newtonsoft.Json;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
-using osu.Game.Tests.Beatmaps;
-using osu.Game.Utils;
namespace osu.Game.Tests.Visual.OnlinePlay
{
- ///
- /// Represents a handler which pretends to be a server, handling room retrieval and manipulation requests
- /// and returning a roughly expected state, without the need for a server to be running.
- ///
- public class TestRoomRequestsHandler
+ public interface IAPIRequestHandler
{
- public IReadOnlyList ServerSideRooms => serverSideRooms;
- private readonly List serverSideRooms = new List();
-
- private int currentRoomId = 1;
- private int currentPlaylistItemId = 1;
- private int currentScoreId = 1;
-
- ///
- /// Handles an API request, while also updating the local state to match how the server would eventually respond.
- ///
- /// The API request to handle.
- /// The local user to store in responses where required.
- /// The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps.
- /// Whether the request was successfully handled.
+ bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager);
+ }
+
+ public class TestRoomRequestsHandler : IAPIRequestHandler
+ {
+ public List ServerSideRooms = new List();
+
+ private static long currentRoomId = 10000;
+ private static long currentPlaylistItemId = 10000;
+ private static long currentScoreId = 10000;
+
public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager)
{
switch (request)
{
case CreateRoomRequest createRoomRequest:
+ {
var apiRoom = cloneRoom(createRoomRequest.Room);
// Passwords are explicitly not copied between rooms.
@@ -54,14 +39,23 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager
AddServerSideRoom(apiRoom, localUser);
var responseRoom = new APICreatedRoom();
- responseRoom.CopyFrom(createResponseRoom(apiRoom, false));
+ if (createResponseRoom(apiRoom, false) is Room res)
+ responseRoom.CopyFrom(res);
+
+ // Propagate back to the source room object used by the test.
+ createRoomRequest.Room.RoomID = apiRoom.RoomID;
+ createRoomRequest.Room.StartDate = apiRoom.StartDate;
+ createRoomRequest.Room.EndDate = apiRoom.EndDate;
+ createRoomRequest.Room.Playlist = apiRoom.Playlist.Select(p => p.With()).ToList();
createRoomRequest.TriggerSuccess(responseRoom);
return true;
+ }
case JoinRoomRequest joinRoomRequest:
{
- var room = ServerSideRooms.Single(r => r.RoomID == joinRoomRequest.Room.RoomID);
+ var room = ServerSideRooms.FirstOrDefault(r => r.RoomID == joinRoomRequest.Room.RoomID);
+ if (room == null) return false;
if (joinRoomRequest.Password != room.Password)
{
@@ -69,99 +63,31 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager
return true;
}
- joinRoomRequest.TriggerSuccess(createResponseRoom(room, true));
+ if (createResponseRoom(room, true) is Room joinRes)
+ joinRoomRequest.TriggerSuccess(joinRes);
return true;
}
- case GetRoomLeaderboardRequest roomLeaderboardRequest:
- roomLeaderboardRequest.TriggerSuccess(new APILeaderboard
- {
- Leaderboard = new List
- {
- new APIUserScoreAggregate
- {
- TotalScore = 1000000,
- TotalAttempts = 5,
- CompletedBeatmaps = 2,
- User = new APIUser { Username = "best user" }
- },
- new APIUserScoreAggregate
- {
- TotalScore = 50,
- TotalAttempts = 1,
- CompletedBeatmaps = 1,
- User = new APIUser { Username = "worst user" }
- }
- }
- });
- return true;
-
- case IndexPlaylistScoresRequest roomLeaderboardRequest:
- roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores
- {
- Scores =
- {
- new MultiplayerScore
- {
- ID = currentScoreId++,
- Accuracy = 1,
- Position = 1,
- EndedAt = DateTimeOffset.Now,
- Passed = true,
- Rank = ScoreRank.S,
- MaxCombo = 1000,
- TotalScore = 1000000,
- User = new APIUser { Username = "best user" },
- Mods = [new APIMod { Acronym = @"DT" }],
- Statistics = new Dictionary()
- },
- new MultiplayerScore
- {
- ID = currentScoreId++,
- Accuracy = 0.7,
- Position = 2,
- EndedAt = DateTimeOffset.Now,
- Passed = true,
- Rank = ScoreRank.B,
- MaxCombo = 100,
- TotalScore = 200000,
- User = new APIUser { Username = "worst user" },
- Mods = [new APIMod { Acronym = @"TD" }],
- Statistics = new Dictionary()
- },
- },
- UserScore = new MultiplayerScore
- {
- ID = currentScoreId++,
- Accuracy = 0.91,
- Position = 4,
- EndedAt = DateTimeOffset.Now,
- Passed = true,
- Rank = ScoreRank.A,
- MaxCombo = 100,
- TotalScore = 800000,
- User = localUser,
- Statistics = new Dictionary()
- },
- });
- return true;
-
- case PartRoomRequest partRoomRequest:
- partRoomRequest.TriggerSuccess();
- return true;
-
case GetRoomsRequest getRoomsRequest:
+ {
var roomsWithoutParticipants = new List();
foreach (var r in ServerSideRooms)
- roomsWithoutParticipants.Add(createResponseRoom(r, false));
+ {
+ if (createResponseRoom(r, false) is Room roomsRes)
+ roomsWithoutParticipants.Add(roomsRes);
+ }
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
return true;
+ }
case GetRoomRequest getRoomRequest:
- getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID == getRoomRequest.RoomId), true));
+ {
+ if (createResponseRoom(ServerSideRooms.FirstOrDefault(r => r.RoomID == getRoomRequest.RoomId), true) is Room getRes)
+ getRoomRequest.TriggerSuccess(getRes);
return true;
+ }
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
@@ -171,150 +97,107 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
{
ID = currentScoreId++,
- Accuracy = 1,
- EndedAt = DateTimeOffset.Now,
- Passed = true,
- Rank = ScoreRank.S,
- MaxCombo = 1000,
- TotalScore = 1000000,
User = localUser,
- Statistics = new Dictionary()
+ Rank = Scoring.ScoreRank.S,
});
return true;
- case GetBeatmapRequest getBeatmapRequest:
- {
- getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.OnlineID).Single());
- return true;
- }
-
- case GetBeatmapsRequest getBeatmapsRequest:
- {
- getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps(getBeatmapsRequest.BeatmapIds.ToArray()) });
- return true;
- }
-
- case GetBeatmapSetRequest getBeatmapSetRequest:
- {
- var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId
- ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID)
- : beatmapManager.QueryBeatmapSet(s => s.OnlineID == getBeatmapSetRequest.ID)?.PerformRead(s => s.Beatmaps.First().Detach());
-
- if (baseBeatmap == null)
+ case GetRoomLeaderboardRequest getRoomLeaderboardRequest:
+ getRoomLeaderboardRequest.TriggerSuccess(new APILeaderboard
{
- baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo;
- baseBeatmap.OnlineID = getBeatmapSetRequest.ID;
- baseBeatmap.BeatmapSet!.OnlineID = getBeatmapSetRequest.ID;
- }
-
- getBeatmapSetRequest.TriggerSuccess(OsuTestScene.CreateAPIBeatmapSet(baseBeatmap));
+ Leaderboard =
+ [
+ new APIUserScoreAggregate
+ {
+ User = localUser,
+ Accuracy = 1,
+ TotalScore = 1000000,
+ },
+ new APIUserScoreAggregate
+ {
+ User = new APIUser { Username = "other user" },
+ Accuracy = 0.5,
+ TotalScore = 500000,
+ }
+ ]
+ });
return true;
- }
- case GetUsersRequest getUsersRequest:
- {
- getUsersRequest.TriggerSuccess(new GetUsersResponse
+ case IndexPlaylistScoresRequest indexPlaylistScoresRequest:
+ indexPlaylistScoresRequest.TriggerSuccess(new IndexedMultiplayerScores
{
- Users = getUsersRequest.UserIds.Select(id => id == TestUserLookupCache.UNRESOLVED_USER_ID
- ? null
- : new APIUser
- {
- Id = id,
- Username = $"User {id}",
- Team = RNG.NextBool()
- ? new APITeam
- {
- Name = "Collective Wangs",
- ShortName = "WANG",
- FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg",
- }
- : null,
- })
- .Where(u => u != null).ToList(),
+ Scores =
+ [
+ new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ User = localUser,
+ Rank = Scoring.ScoreRank.S,
+ }
+ ],
+ UserScore = new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ User = localUser,
+ Rank = Scoring.ScoreRank.A,
+ }
});
return true;
- }
- }
- List createResponseBeatmaps(params int[] beatmapIds)
- {
- var result = new List();
-
- foreach (int id in beatmapIds)
+ case GetBeatmapRequest getBeatmapRequest:
{
- var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id);
-
- if (baseBeatmap == null)
- {
- baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo;
- baseBeatmap.OnlineID = id;
- baseBeatmap.BeatmapSet!.OnlineID = id;
- }
-
- result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap));
+ if (createResponseBeatmaps(getBeatmapRequest.OnlineID).FirstOrDefault() is APIBeatmap bm)
+ getBeatmapRequest.TriggerSuccess(bm);
+ return true;
}
-
- return result;
}
return false;
}
- ///
- /// Adds a room to a local "server-side" list that's returned when a is fired.
- ///
- /// The room.
- /// The room host.
- public void AddServerSideRoom(Room room, APIUser host)
+ public void AddServerSideRoom(Room room, APIUser user)
{
room.RoomID = currentRoomId++;
- room.Host = host;
+ room.Host = user;
+
+ room.StartDate ??= DateTimeOffset.Now;
- for (int i = 0; i < room.Playlist.Count; i++)
+ foreach (var item in room.Playlist)
{
- room.Playlist[i].ID = currentPlaylistItemId++;
- room.Playlist[i].OwnerID = room.Host.OnlineID;
+ if (item.ID == 0)
+ item.ID = currentPlaylistItemId++;
}
- serverSideRooms.Add(room);
- }
-
- private Room createResponseRoom(Room room, bool withParticipants)
- {
- var responseRoom = cloneRoom(room);
-
- // Password is hidden from the response, and is only propagated via HasPassword.
- responseRoom.Password = responseRoom.HasPassword ? Guid.NewGuid().ToString() : null;
-
- if (!withParticipants)
- responseRoom.RecentParticipants = [];
-
- return responseRoom;
+ ServerSideRooms.Add(room);
}
private Room cloneRoom(Room source)
{
- var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source));
- Debug.Assert(result != null);
+ var result = new Room();
+ result.CopyFrom(source);
result.RoomID = source.RoomID;
result.StartDate = source.StartDate;
result.EndDate = source.EndDate;
+ result.Host = source.Host;
+ result.Playlist = source.Playlist.Select(p => p.With()).ToList();
+ return result;
+ }
+
+ private Room? createResponseRoom(Room? room, bool withParticipants)
+ {
+ if (room == null) return null;
- // When serialising, only beatmap IDs are sent to the server.
- // When deserialising, full beatmaps and IDs are expected to arrive.
+ var responseRoom = cloneRoom(room);
- PlaylistItem? finalCurrentItem = result.CurrentPlaylistItem != null && source.CurrentPlaylistItem != null
- ? result.CurrentPlaylistItem.With(id: source.CurrentPlaylistItem.ID, beatmap: new Optional(source.CurrentPlaylistItem.Beatmap))
- : null;
- PlaylistItem[] finalPlaylist = result.Playlist.Select((pi, i) => pi.With(id: source.Playlist[i].ID, beatmap: new Optional(source.Playlist[i].Beatmap))).ToArray();
+ if (!withParticipants)
+ responseRoom.ParticipantCount = 0;
- // When setting the properties, we do a clear-then-add, otherwise equality comparers (that only compare by ID) pass early and members don't get replaced.
- result.CurrentPlaylistItem = null;
- result.CurrentPlaylistItem = finalCurrentItem;
- result.Playlist = [];
- result.Playlist = finalPlaylist;
+ return responseRoom;
+ }
- return result;
+ private IEnumerable createResponseBeatmaps(int onlineID)
+ {
+ yield return new APIBeatmap { OnlineID = onlineID };
}
}
}
diff --git a/vulkan_fix.patch b/vulkan_fix.patch
deleted file mode 100644
index 492baddbbe1b..000000000000
--- a/vulkan_fix.patch
+++ /dev/null
@@ -1,16 +0,0 @@
-<<<<<<< SEARCH
- public OsuGameAndroid(OsuGameActivity activity)
- : base(null)
- {
- gameActivity = activity;
- }
-=======
- public OsuGameAndroid(OsuGameActivity activity)
- : base(null)
- {
- gameActivity = activity;
-
- // Start Vulkan probe as early as possible so it's ready for SetHost.
- startVulkanProbe();
- }
->>>>>>> REPLACE