diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5a616aaab62..6d52ee1db569 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: -p:Version="${{ steps.version.outputs.version }}" \ -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \ -p:ApplicationVersion="${{ github.run_number }}" \ - -p:AndroidKeyStore=true \ + -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 }}" \ @@ -113,7 +113,7 @@ jobs: -p:Version="${{ steps.version.outputs.version }}" \ -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \ -p:ApplicationVersion="${{ github.run_number }}" \ - -p:AndroidKeyStore=false + -p:AndroidKeyStore="false" - name: Find APK id: find_apk diff --git a/build/PatchElfPageSize.targets b/build/PatchElfPageSize.targets index 9be15f4da475..815e1b4aacd6 100644 --- a/build/PatchElfPageSize.targets +++ b/build/PatchElfPageSize.targets @@ -1,17 +1,3 @@ - @@ -21,250 +7,131 @@ 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); + WasPatched = false; + int pageSize = TargetPageSize > 0 ? TargetPageSize : 0x4000; -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) + if (System.IO.File.Exists(FilePath)) { - System.Threading.Thread.Sleep(1000); - } - } - - 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; - } + string lockFile = FilePath + ".patch-lock"; + System.IO.FileStream lockStream = null; - // 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) + try { - stillNeeds = true; - break; + 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) { lockStream.Dispose(); try { System.IO.File.Delete(lockFile); } catch { } } } } - } - - 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/osu.Android.props b/osu.Android.props index 513521619ead..619daef573a8 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -5,23 +5,21 @@ apk CJK;Mideast;Rare;West;Other; Xamarin.Android.Net.AndroidMessageHandler - true true + + + $(NoWarn);XA1006 + $(MSBuildWarningsAsMessages);XA1006 + false - - true true @@ -32,30 +30,18 @@ - - - - - true - diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index b0e837ed958c..0aff0dca2113 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -1,6 +1,6 @@ - + diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 0e6b03cd6ebc..2a18de49b934 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -47,6 +47,10 @@ 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.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml index 16be596df96b..b7755dd48e3f 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 c308a3d4a3e4..1671c8cb1adc 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 93b0d73e85c8..d12fb104f88a 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.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml index f8213d7936e5..0b1feae5d8e4 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 48fa69445873..f9fa3d105472 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/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 44c2e7eb55fd..090a5e71242b 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", () => match.SelectedItem.Value == room.Playlist[0]); + AddAssert("first playlist item selected", () => room.Playlist.Count > 0 && 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 a3f6fa6671df..7313679bc809 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[0]); + AddStep("select first item", () => screen.SelectedItem.Value = room.Playlist.FirstOrDefault()); 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[0]); + AddStep("select first playlist item", () => screen.SelectedItem.Value = room.Playlist.FirstOrDefault()); // 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 560ac77f8f73..8bd6d28daa56 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -182,7 +182,7 @@ private set /// /// The corresponding to the local player, if available. /// - public virtual MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); + public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.Id); /// /// Whether the is the host in . @@ -306,7 +306,7 @@ 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.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + APIRoom.CurrentPlaylistItem = APIRoom.Playlist.FirstOrDefault(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; @@ -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.Single(item => item.ID == settings.PlaylistItemId); + APIRoom.CurrentPlaylistItem = APIRoom.Playlist.FirstOrDefault(item => item.ID == settings.PlaylistItemId); APIRoom.AutoSkip = Room.Settings.AutoSkip; SettingsChanged?.Invoke(settings); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 09c0c3f01746..bda1135c879c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -61,7 +61,7 @@ public override bool Remove(Drawable drawable, bool disposeImmediately) { int index = content.IndexOf(drawable); - if (index > 0) + if (index >= 0) navigationFlow.Remove(navigationFlow[index], true); return base.Remove(drawable, disposeImmediately); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 32be5a8851a2..9ca650c0444e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -1,9 +1,10 @@ // 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.Screens; +using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Localisation; @@ -18,8 +19,6 @@ public partial class NewDailyChallengeNotification : SimpleNotification { private readonly Room room; - private BeatmapCardNano card = null!; - public NewDailyChallengeNotification(Room room) { this.room = room; @@ -29,7 +28,18 @@ public NewDailyChallengeNotification(Room room) private void load(OsuGame? game, SessionStatics statics) { Text = DailyChallengeStrings.ChallengeLiveNotification; - Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); + + var item = room.Playlist.FirstOrDefault(); + + if (item?.Beatmap.BeatmapSet is APIBeatmapSet beatmapSet) + { + Content.Add(new BeatmapCardNano(beatmapSet) + { + RelativeSizeAxes = Axes.X, + Width = 1, + }); + } + Activated = () => { if (statics.Get(Static.DailyChallengeIntroPlayed)) @@ -40,11 +50,5 @@ 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/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index ce14d0bb19c0..04d72185cde1 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -123,7 +123,7 @@ private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true; + if (panels.FirstOrDefault(p => p.RoomUser.Equals(user)) is { } panel) panel.HasQuit = true; updateDisplay(); }); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index a06aa633379f..9aa11b7f30dc 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -45,7 +45,7 @@ public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable [Resolved] private SpectatorClient client { get; set; } = null!; - [Resolved] + [Resolved(CanBeNull = true)] private GameplayState gameplayState { get; set; } = null!; [Resolved] @@ -87,7 +87,7 @@ protected override void LoadComplete() { base.LoadComplete(); - ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); + if (gameplayState != null) ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d1691d59ad01..de62ed25394e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -147,7 +147,7 @@ public void RemoveUser(APIUser user) ((IMultiplayerClient)this).UserLeft(clone(new MultiplayerRoomUser(user.Id))); if (ServerRoom.Users.Any()) - TransferHost(ServerRoom.Users.First().UserID); + TransferHost(ServerRoom.Users.FirstOrDefault()?.UserID ?? 0); } 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.Max(item => item.ID); + lastPlaylistItemId = ServerAPIRoom.Playlist.Any() ? ServerAPIRoom.Playlist.Max(item => item.ID) : 0; var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) { @@ -741,7 +741,10 @@ 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).First(); + MultiplayerPlaylistItem? nextItem = upcomingItems.FirstOrDefault() ?? ServerRoom.Playlist.OrderByDescending(i => i.PlayedAt).FirstOrDefault(); + + if (nextItem == null) + return; currentIndex = ServerRoom.Playlist.IndexOf(nextItem); @@ -811,7 +814,16 @@ private async Task updatePlaylistOrder(MultiplayerRoom room) private T clone(T incoming) { byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); - return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); + var result = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); + + if (result is MultiplayerRoom room) + { + if (room.Host is { } host) host.User = ServerRoom!.Users.FirstOrDefault(u => u.UserID == host.UserID)?.User; + + foreach (var user in room.Users) user.User = ServerRoom!.Users.FirstOrDefault(u => u.UserID == user.UserID)?.User; + } + + return result; } public override Task DisconnectInternal() diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 08f61f3ddcdf..1f7296e0af03 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -296,11 +296,16 @@ private Room cloneRoom(Room source) { var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); Debug.Assert(result != null); + result.RoomID = source.RoomID; + result.StartDate = source.StartDate; + result.EndDate = source.EndDate; // When serialising, only beatmap IDs are sent to the server. // When deserialising, full beatmaps and IDs are expected to arrive. - PlaylistItem? finalCurrentItem = result.CurrentPlaylistItem?.With(id: source.CurrentPlaylistItem!.ID, beatmap: new Optional(source.CurrentPlaylistItem.Beatmap)); + 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(); // 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.