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
594 lines
18 KiB
C#
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
|
|
}
|
|
} |