Files
McSchneidar cf2fa796e2 .NET10 Migration (#946)
Runtime and build
- Move all projects to .NET 10 with SDK-style project files and a shared
  Directory.Build.props (WIN/LINUX, x86/x64 defines)
- Load managed and unmanaged dependencies from libs/managed and
  libs/unmanaged through assembly and native library resolvers
- Compile party modes with Roslyn instead of CodeDom
- Update AppVeyor to the .NET 10 SDK and dotnet test
- Download the FFmpeg and gstreamer runtimes from the dependencies
  repository during the build
- Remove the VC++ 2015-2022 runtime check, no dependency needs it anymore

Graphics and windowing
- Replace the Direct3D 9 renderer with Direct3D 11 (Vortice)
- Pace Direct3D 11 frames with a frame latency waitable swap chain
- Move window, input and message boxes to SDL3
- Replace System.Drawing with SkiaSharp and WinForms with SDL3 and a
  software renderer

Video and audio
- Replace Acinerella with FFmpeg 8.1.3 (FFmpeg.AutoGen)
- Add hardware accelerated video decoding (D3D11VA, DXVA2, VideoToolbox,
  VAAPI, VDPAU) with software fallback
- Convert YUV video frames to RGB on the GPU in Direct3D 11 and OpenGL
- Add the video options HardwareDecoding, GpuColorConversion,
  DecoderThreads and VideoDownscaleResolution
- Update OpenAL to OpenTK 4 and NAudio recording to WasapiRecorder

Fixes
- Fix the song preview start and shorten the reporter buttons
- Fix a crash when opening the highscore without a selected song
- Use the options slide highlight in the party mode screens to avoid
  brightness flicker on VRR displays
- Disable GameInput in the x86 build, SDL3 crashes in GameInputCreate
  on 32-bit systems with the GameInput runtime installed
- Fix a truncation warning in PitchTracker
2026-11-02 08:11:20 +01:00

594 lines
18 KiB
C#

#region license
// This file is part of Vocaluxe.
//
// Vocaluxe is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vocaluxe is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
using VocaluxeLib.Draw;
namespace VocaluxeLib.Menu
{
[XmlType("NameSelection")]
public struct SThemeNameSelection
{
[XmlAttribute(AttributeName = "Name")]
public string Name;
public SRectF Rect;
public string SkinEmptyTile;
public SThemeColor ColorEmptyTile;
public string SkinTileSelected;
public SThemeNameSelectionTiles Tiles;
}
public struct SThemeNameSelectionTiles
{
public int W;
public int H;
public int NumW;
public int NumH;
public float SpaceW;
public float SpaceH;
public SThemeNameSelectionName Name;
}
public struct SThemeNameSelectionName
{
public float Space;
[XmlElement("H")]
public float Height;
public string Font;
public EStyle Style;
public SThemeColor Color;
}
public class CNameSelection : CMenuElementBase, IMenuElement, IThemeable
{
private class CTile
{
public Guid ProfileId;
public readonly CStatic Avatar;
public readonly CText Name;
public CTile(CStatic av, CText tex, Guid pId)
{
Avatar = av;
Name = tex;
ProfileId = pId;
}
}
private readonly int _PartyModeId;
private SThemeNameSelection _Theme;
public string GetThemeName()
{
return _Theme.Name;
}
public bool ThemeLoaded { get; private set; }
private readonly List<CTile> _Tiles;
private CTextureRef _TextureTileSelected;
public CTextureRef TextureEmptyTile { get; private set; }
private SColorF _ColorEmptyTile;
private SColorF _ColorNameTile;
public int Offset;
private int _ActualSelection = -1;
public Guid SelectedId = Guid.Empty;
private int _Player = -1;
private readonly List<Guid> _VisibleProfiles;
private readonly List<Guid> _UsedProfiles;
private readonly HashSet<Guid> _InactiveProfiles = new HashSet<Guid>();
public bool ShowInactive;
private CStatic _PlayerSelector;
private SRectF _Rect;
public override SRectF Rect
{
get { return _Rect; }
}
public bool Selectable
{
get { return Visible; }
}
public CNameSelection(int partyModeId)
{
_PartyModeId = partyModeId;
_Theme = new SThemeNameSelection { Tiles = new SThemeNameSelectionTiles { Name = new SThemeNameSelectionName() } };
_Tiles = new List<CTile>();
_VisibleProfiles = new List<Guid>();
_UsedProfiles = new List<Guid>();
}
public CNameSelection(SThemeNameSelection theme, int partyModeId)
{
_PartyModeId = partyModeId;
_Theme = theme;
_Tiles = new List<CTile>();
_VisibleProfiles = new List<Guid>();
_UsedProfiles = new List<Guid>();
ThemeLoaded = true;
}
public void Init()
{
_UsedProfiles.Clear();
_PrepareTiles();
_PlayerSelector = new CStatic(_PartyModeId, _TextureTileSelected, new SColorF(), new SRectF(0, 0, _Theme.Tiles.W + 10, _Theme.Tiles.H + 10, Rect.Z - 0.5f))
{
Visible = true
};
_UpdateVisibleProfiles();
UpdateList(0);
_ActualSelection = _Tiles.FindIndex(t => t.ProfileId != Guid.Empty);
SelectedId = _ActualSelection >= 0 ? _Tiles[_ActualSelection].ProfileId : Guid.Empty;
}
public void Draw()
{
var i = 0;
foreach (var tile in _Tiles)
{
tile.Avatar.Draw();
tile.Name.Draw();
if (_PlayerSelector.Visible)
{
//Update PlayerSelector-Coords
if (_Player > -1 && _ActualSelection == i)
{
_PlayerSelector.X = tile.Avatar.Rect.X - 3;
_PlayerSelector.Y = tile.Avatar.Rect.Y - 3;
}
}
i++;
}
if (_PlayerSelector.Visible)
{
//Draw PlayerSelector
_PlayerSelector.Draw();
}
}
public void HandleInput(SKeyEvent kevent)
{
if (_Tiles.Count == 0 || _VisibleProfiles.Count == 0)
{
_ActualSelection = -1;
SelectedId = Guid.Empty;
return;
}
if (_ActualSelection < 0 || _ActualSelection >= _Tiles.Count || _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection = _Tiles.FindIndex(t => t.ProfileId != Guid.Empty);
if (_ActualSelection < 0)
{
SelectedId = Guid.Empty;
return;
}
}
switch (kevent.Key)
{
case Keys.Right:
if (_ActualSelection + 1 < _Tiles.Count)
{
if (_Tiles[_ActualSelection + 1].ProfileId != Guid.Empty)
{
_ActualSelection++;
}
}
else
{
var oldOffset = Offset;
UpdateList(Offset + 1);
if (oldOffset != Offset)
{
_ActualSelection = Math.Max(0, _ActualSelection - (_Theme.Tiles.NumW - 1));
}
}
break;
case Keys.Left:
if (_ActualSelection - 1 >= 0)
{
_ActualSelection--;
}
else if (Offset > 0)
{
UpdateList(Offset - 1);
_ActualSelection = Math.Min(_Tiles.Count - 1, _ActualSelection + (_Theme.Tiles.NumW - 1));
while (_ActualSelection >= 0 && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection--;
}
}
break;
case Keys.Up:
if (_ActualSelection - _Theme.Tiles.NumW >= 0)
{
_ActualSelection -= _Theme.Tiles.NumW;
}
else if (Offset > 0)
{
var currentColumn = _ActualSelection % _Theme.Tiles.NumW;
UpdateList(Offset - 1);
_ActualSelection = currentColumn;
if (_ActualSelection >= _Tiles.Count)
{
_ActualSelection = _Tiles.Count - 1;
}
while (_ActualSelection >= 0 && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection--;
}
}
break;
case Keys.Down:
if (_ActualSelection + _Theme.Tiles.NumW < _Tiles.Count &&
_Tiles[_ActualSelection + _Theme.Tiles.NumW].ProfileId != Guid.Empty)
{
_ActualSelection += _Theme.Tiles.NumW;
}
else
{
var oldOffset = Offset;
var currentColumn = _ActualSelection % _Theme.Tiles.NumW;
UpdateList(Offset + 1);
if (oldOffset != Offset)
{
_ActualSelection = currentColumn;
while (_ActualSelection < _Tiles.Count && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection++;
}
if (_ActualSelection >= _Tiles.Count || _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection = _Tiles.FindLastIndex(t => t.ProfileId != Guid.Empty);
}
}
}
break;
}
if (_ActualSelection >= 0 && _ActualSelection < _Tiles.Count && _Tiles[_ActualSelection].ProfileId != Guid.Empty)
{
SelectedId = _Tiles[_ActualSelection].ProfileId;
}
else
{
SelectedId = Guid.Empty;
}
}
public void HandleMouse(SMouseEvent mevent)
{
for (var i = 0; i < _Tiles.Count; i++)
{
if (CHelper.IsInBounds(_Tiles[i].Avatar.Rect, mevent) && _Tiles[i].ProfileId != Guid.Empty)
{
_ActualSelection = i;
if (Offset * _Theme.Tiles.NumW + _ActualSelection < _VisibleProfiles.Count)
{
SelectedId = _VisibleProfiles.ElementAt(Offset * _Theme.Tiles.NumW + _ActualSelection);
}
else
{
SelectedId = Guid.Empty;
}
}
}
}
public void FastSelection(bool active, int player)
{
//Overwrite player-selection; Same profile, but other player
if (active && SelectedId != Guid.Empty)
{
_Player = player;
_PlayerSelector.Color = CBase.Themes.GetPlayerColor(player);
}
//Normal activation
else if (active)
{
if (_VisibleProfiles.Count > 0)
{
SelectedId = _VisibleProfiles.ElementAt(0);
_ActualSelection = 0;
}
else
{
SelectedId = Guid.Empty;
_ActualSelection = -1;
}
_Player = player;
_PlayerSelector.Color = CBase.Themes.GetPlayerColor(player);
_PlayerSelector.Visible = true;
}
//Deactivate
else
{
SelectedId = Guid.Empty;
_ActualSelection = -1;
_Player = -1;
_PlayerSelector.Visible = false;
}
}
public void UpdateList()
{
_UpdateVisibleProfiles();
if (_Theme.Tiles.NumW * (Offset + 1) - _VisibleProfiles.Count >= _Theme.Tiles.NumW * Offset)
{
UpdateList(Offset - 1);
}
else
{
UpdateList(Offset);
}
}
public void UpdateList(int offset)
{
offset = offset.Clamp(0, _GetMaxOffset());
for (var i = 0; i < _Tiles.Count; i++)
{
if (i + offset * _Theme.Tiles.NumW < _VisibleProfiles.Count)
{
var alpha = _InactiveProfiles.Contains(_VisibleProfiles[i + offset * _Theme.Tiles.NumW]) ? 0.4f : 1f;
_Tiles[i].Avatar.Texture = CBase.Profiles.GetAvatar(_VisibleProfiles[i + offset * _Theme.Tiles.NumW]);
_Tiles[i].Avatar.Color = new SColorF(1, 1, 1, alpha);
_Tiles[i].Name.Alpha = alpha;
_Tiles[i].Name.Text = CBase.Profiles.GetPlayerName(_VisibleProfiles[i + offset * _Theme.Tiles.NumW]);
_Tiles[i].ProfileId = _VisibleProfiles.ElementAt(i + offset * _Theme.Tiles.NumW);
}
else
{
_Tiles[i].Avatar.Texture = TextureEmptyTile;
_Tiles[i].Avatar.Color = _ColorEmptyTile;
_Tiles[i].Name.Alpha = 1f;
_Tiles[i].Name.Text = "";
_Tiles[i].ProfileId = Guid.Empty;
}
}
Offset = offset;
}
public bool IsOverTile(SMouseEvent mevent)
{
var isOver = false;
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
isOver = true;
}
}
return isOver;
}
public Guid TilePlayerId(SMouseEvent mevent)
{
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
return tile.ProfileId;
}
}
return Guid.Empty;
}
public CStatic TilePlayerAvatar(SMouseEvent mevent)
{
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
return tile.Avatar;
}
}
return new CStatic(_PartyModeId);
}
public void UnloadSkin() { }
public void LoadSkin()
{
TextureEmptyTile = CBase.Themes.GetSkinTexture(_Theme.SkinEmptyTile, _PartyModeId);
_TextureTileSelected = CBase.Themes.GetSkinTexture(_Theme.SkinTileSelected, _PartyModeId);
_Theme.ColorEmptyTile.Get(_PartyModeId, out _ColorEmptyTile);
_Theme.Tiles.Name.Color.Get(_PartyModeId, out _ColorNameTile);
MaxRect = _Theme.Rect;
}
public void ReloadSkin()
{
UnloadSkin();
LoadSkin();
_PrepareTiles();
}
public void UseProfile(Guid id)
{
if (id != Guid.Empty)
{
if (!_UsedProfiles.Contains(id) && CBase.Profiles.IsProfileIdValid(id) && !CBase.Profiles.IsGuest(id))
{
_UsedProfiles.Add(id);
UpdateList();
}
}
}
public void RemoveUsedProfile(Guid id)
{
if (id != Guid.Empty)
{
_UsedProfiles.Remove(id);
UpdateList();
}
}
public Guid GetRandomUnusedProfile()
{
var unusedProfiles = _VisibleProfiles.Where(profileId => !_UsedProfiles.Contains(profileId)).ToList();
if (unusedProfiles.Count == 0)
{
return Guid.Empty;
}
return unusedProfiles[CBase.Game.GetRandom(unusedProfiles.Count)];
}
private void _PrepareTiles()
{
_Tiles.Clear();
for (var i = 0; i < _Theme.Tiles.NumH; i++)
{
for (var j = 0; j < _Theme.Tiles.NumW; j++)
{
var rect = new SRectF(MaxRect.X + j * (_Theme.Tiles.W + _Theme.Tiles.SpaceW), MaxRect.Y + i * (_Theme.Tiles.H + _Theme.Tiles.SpaceH),
_Theme.Tiles.W, _Theme.Tiles.H, MaxRect.Z);
var tileStatic = new CStatic(_PartyModeId, TextureEmptyTile, _ColorEmptyTile, rect) { Aspect = EAspect.Crop };
var tileText = new CText(rect.X + rect.W / 2, rect.Y + rect.H + _Theme.Tiles.Name.Space, rect.Z, _Theme.Tiles.Name.Height, rect.W, EAlignment.Center,
_Theme.Tiles.Name.Style, _Theme.Tiles.Name.Font, _ColorNameTile, "");
_Tiles.Add(new CTile(tileStatic, tileText, Guid.Empty));
}
}
_Rect.X = MaxRect.X;
_Rect.Y = MaxRect.Y;
_Rect.Right = _Tiles[_Tiles.Count - 1].Avatar.Rect.Right;
_Rect.Bottom = _Tiles[_Tiles.Count - 1].Name.Rect.Bottom;
_Rect.Z = MaxRect.Z;
}
private void _UpdateVisibleProfiles()
{
_VisibleProfiles.Clear();
_InactiveProfiles.Clear();
var profiles = CBase.Profiles.GetProfiles();
foreach (var profile in profiles)
{
var visible = profile.Active == EOffOn.TR_CONFIG_ON;
if (!visible && ShowInactive)
{
visible = true;
_InactiveProfiles.Add(profile.Id);
}
if (visible)
{
if (_UsedProfiles.Contains(profile.Id) && (int)profile.UserRole >= (int)EUserRole.TR_USERROLE_NORMAL)
{
visible = false;
}
}
if (visible)
{
_VisibleProfiles.Add(profile.Id);
}
}
}
private int _GetMaxOffset()
{
var maxOffset = (int)Math.Ceiling((decimal)(_VisibleProfiles.Count - _Tiles.Count) / _Theme.Tiles.NumW);
if (maxOffset < 0)
{
return 0;
}
return maxOffset;
}
public object GetTheme()
{
return _Theme;
}
#region ThemeEdit
public void MoveElement(int stepX, int stepY)
{
X += stepX;
Y += stepY;
_Theme.Rect.X += stepX;
_Theme.Rect.Y += stepY;
}
public void ResizeElement(int stepW, int stepH)
{
W += stepW;
if (W <= 0)
{
W = 1;
}
H += stepH;
if (H <= 0)
{
H = 1;
}
_Theme.Rect.W = W;
_Theme.Rect.H = H;
}
#endregion ThemeEdit
}
}