Files
Vocaluxe/PartyModes/PartyModeRuleRoulette/CPartyScreenRuleRouletteMain.cs
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

622 lines
23 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.Diagnostics;
using VocaluxeLib.Menu;
namespace VocaluxeLib.PartyModes.RuleRoulette
{
class CWheelCard
{
public CStatic Active;
public CStatic Box;
public CText Name;
}
class CTableRow
{
public CStatic Highlight;
public CStatic Avatar;
public CText Pos;
public CText Name;
public CText Points;
}
public class CPartyScreenRuleRouletteMain : CPartyScreenRuleRoulette
{
protected override int _ScreenVersion
{
get { return 1; }
}
private const string _TextPopupReallyExit = "TextPopupReallyExit";
private const string _TextDuel = "TextDuel";
private const string _TextRowPos = "TextRowPos";
private const string _TextRowName = "TextRowName";
private const string _TextRowPoints = "TextRowPoints";
private const string _TextCardName = "TextCardName";
private const string _TextCardNameResult = "TextCardNameResult";
private const string _TextName1 = "TextName1";
private const string _TextName2 = "TextName2";
private const string _TextVersus = "TextVersus";
private const string _TextRuleTitle = "TextRuleTitle";
private const string _TextRuleDescription = "TextRuleDescription";
private const string _TextRuleInfo = "TextRuleInfo";
private const string _TextLastResult = "TextLastResult";
private const string _TextChampionLabel = "TextChampionLabel";
private const string _TextChampion = "TextChampion";
private const string _ButtonNextRound = "ButtonNextRound";
private const string _ButtonBack = "ButtonBack";
private const string _ButtonExit = "ButtonExit";
private const string _ButtonPopupYes = "ButtonPopupYes";
private const string _ButtonPopupNo = "ButtonPopupNo";
private const string _StaticPopupBG = "StaticPopupBG";
private const string _StaticRowHighlight = "StaticRowHighlight";
private const string _StaticRowAvatar = "StaticRowAvatar";
private const string _StaticCardBox = "StaticCardBox";
private const string _StaticCardActive = "StaticCardActive";
private const string _StaticAvatar1 = "StaticAvatar1";
private const string _StaticAvatar2 = "StaticAvatar2";
private const string _ParticleEffectChampion = "ParticleEffectChampion";
private const float _WheelCenterX = 1240f;
private const float _WheelCenterY = 530f;
private const float _WheelRadiusX = 545f;
private const float _WheelRadiusY = 320f;
private const float _ActiveBorder = 4f;
private const float _UsedAlpha = 0.35f;
private const float _FirstRowY = 255f;
private const float _RowStep = 52f;
private const float _PopDuration = 700f;
private const int _SpinRounds = 3;
private const float _SpinMinStep = 50f;
private const float _SpinMaxStep = 420f;
private readonly string[] _RankColors = new string[] { "Top1Color", "Top2Color", "Top3Color" };
private bool _ExitPopupVisible;
private List<CWheelCard> _Cards;
private List<CTableRow> _Rows;
private readonly Stopwatch _AnimTimer = new Stopwatch();
private readonly Stopwatch _SpinTimer = new Stopwatch();
private List<float> _SpinSteps;
private int _SpinTarget = -1;
private bool _Spinning;
private int _LastSeenDuels = -1;
private bool _ShowNewResult;
private float _LastResultHeight;
private float _ChampionHeight;
private SRectF _Avatar1Rect;
public override void Init()
{
base.Init();
_ThemeTexts = new string[]
{
_TextPopupReallyExit, _TextDuel, _TextRowPos, _TextRowName, _TextRowPoints, _TextCardName, _TextCardNameResult, _TextName1, _TextName2, _TextVersus,
_TextRuleTitle, _TextRuleDescription, _TextRuleInfo, _TextLastResult, _TextChampionLabel, _TextChampion
};
_ThemeButtons = new string[] { _ButtonNextRound, _ButtonBack, _ButtonExit, _ButtonPopupYes, _ButtonPopupNo };
_ThemeStatics = new string[] { _StaticPopupBG, _StaticRowHighlight, _StaticRowAvatar, _StaticCardBox, _StaticCardActive, _StaticAvatar1, _StaticAvatar2 };
_ThemeParticleEffects = new string[] { _ParticleEffectChampion };
}
public override void LoadTheme(string xmlPath)
{
base.LoadTheme(xmlPath);
_Cards = new List<CWheelCard>();
for (var i = 0; i < CPartyModeRuleRoulette.MaxRulesOnWheel; i++)
{
var card = new CWheelCard
{
Active = GetNewStatic(_Statics[_StaticCardActive]),
Box = GetNewStatic(_Statics[_StaticCardBox]),
Name = GetNewText(_Texts[_TextCardName])
};
_AddStatic(card.Active);
_AddStatic(card.Box);
_AddText(card.Name);
_Cards.Add(card);
}
_Rows = new List<CTableRow>();
for (var i = 0; i < _PartyMode.MaxPlayers; i++)
{
var row = new CTableRow
{
Highlight = GetNewStatic(_Statics[_StaticRowHighlight]),
Avatar = GetNewStatic(_Statics[_StaticRowAvatar]),
Pos = GetNewText(_Texts[_TextRowPos]),
Name = GetNewText(_Texts[_TextRowName]),
Points = GetNewText(_Texts[_TextRowPoints])
};
row.Avatar.Aspect = EAspect.Crop;
var y = _FirstRowY + i * _RowStep;
row.Highlight.Y = y + (row.Name.H - row.Highlight.H) / 2;
row.Avatar.Y = y + (row.Name.H - row.Avatar.H) / 2;
row.Pos.Y = y;
row.Name.Y = y;
row.Points.Y = y;
_AddStatic(row.Highlight);
_AddStatic(row.Avatar);
_AddText(row.Pos);
_AddText(row.Name);
_AddText(row.Points);
_Rows.Add(row);
}
_Statics[_StaticCardBox].Visible = false;
_Statics[_StaticCardActive].Visible = false;
_Statics[_StaticRowHighlight].Visible = false;
_Statics[_StaticRowAvatar].Visible = false;
_Texts[_TextCardName].Visible = false;
_Texts[_TextCardNameResult].Visible = false;
_Texts[_TextRowPos].Visible = false;
_Texts[_TextRowName].Visible = false;
_Texts[_TextRowPoints].Visible = false;
_Statics[_StaticAvatar1].Aspect = EAspect.Crop;
_Statics[_StaticAvatar2].Aspect = EAspect.Crop;
_Avatar1Rect = _Statics[_StaticAvatar1].Rect;
_LastResultHeight = _Texts[_TextLastResult].Font.Height;
_ChampionHeight = _Texts[_TextChampion].Font.Height;
}
public override bool HandleInput(SKeyEvent keyEvent)
{
base.HandleInput(keyEvent);
if (!keyEvent.KeyPressed)
{
switch (keyEvent.Key)
{
case Keys.Back:
case Keys.Escape:
_OnBack();
break;
case Keys.Enter:
_OnSelect();
break;
}
}
return true;
}
public override bool HandleMouse(SMouseEvent mouseEvent)
{
base.HandleMouse(mouseEvent);
if (mouseEvent.LB && _IsMouseOverCurSelection(mouseEvent))
{
_OnSelect();
}
if (mouseEvent.RB)
{
_OnBack();
}
return true;
}
public override void OnShow()
{
base.OnShow();
_ShowNewResult = _PartyMode.GameData.LastDuel != null && _PartyMode.GameData.NumDuelsPlayed != _LastSeenDuels;
_LastSeenDuels = _PartyMode.GameData.NumDuelsPlayed;
_Spinning = false;
_SpinTarget = _PartyMode.GameData.RuleChosen && _PartyMode.CurrentDuel != null ? _PartyMode.GameData.Wheel.IndexOf(_PartyMode.CurrentDuel.Rule) : -1;
_LayoutCards();
_UpdateTable();
_UpdateCenter();
_UpdateCards(_SpinTarget);
_UpdateButtons();
_AnimTimer.Reset();
_AnimTimer.Start();
_UpdateAnimations();
_ShowPopup(false);
}
public override bool UpdateGame()
{
if (_Spinning)
{
_UpdateSpin();
}
_UpdateAnimations();
return true;
}
private void _OnBack()
{
if (_Spinning)
{
return;
}
if (_ExitPopupVisible)
{
_ShowPopup(false);
}
else if (_PartyMode.GameData.NumDuelsPlayed == 0 && !_PartyMode.GameData.RuleChosen)
{
_PartyMode.Back();
}
else if (_PartyMode.IsFinished)
{
_EndParty();
}
else
{
_ShowPopup(true);
}
}
private void _OnSelect()
{
if (_Spinning)
{
return;
}
if (_ExitPopupVisible)
{
if (_Buttons[_ButtonPopupYes].Selected)
{
_EndParty();
}
if (_Buttons[_ButtonPopupNo].Selected)
{
_ShowPopup(false);
}
return;
}
if (_Buttons[_ButtonNextRound].Selected && _Buttons[_ButtonNextRound].Visible)
{
if (_PartyMode.GameData.RuleChosen)
{
_PartyMode.Next();
}
else
{
_StartSpin();
}
}
if (_Buttons[_ButtonBack].Selected && _Buttons[_ButtonBack].Visible)
{
_PartyMode.Back();
}
if (_Buttons[_ButtonExit].Selected && _Buttons[_ButtonExit].Visible)
{
if (_PartyMode.IsFinished)
{
_EndParty();
}
else
{
_ShowPopup(true);
}
}
}
private void _EndParty()
{
CBase.Graphics.FadeTo(EScreen.Party);
}
private void _ShowPopup(bool visible)
{
_ExitPopupVisible = visible;
_Statics[_StaticPopupBG].Visible = _ExitPopupVisible;
_Texts[_TextPopupReallyExit].Visible = _ExitPopupVisible;
_Buttons[_ButtonPopupYes].Visible = _ExitPopupVisible;
_Buttons[_ButtonPopupNo].Visible = _ExitPopupVisible;
if (_ExitPopupVisible)
{
_SelectElement(_Buttons[_ButtonPopupNo]);
}
}
private void _UpdateButtons()
{
var started = _PartyMode.GameData.NumDuelsPlayed > 0 || _PartyMode.GameData.RuleChosen;
_Buttons[_ButtonBack].Visible = !started;
_Buttons[_ButtonExit].Visible = started;
_Buttons[_ButtonNextRound].Visible = !_PartyMode.IsFinished;
_Buttons[_ButtonNextRound].Text.Text = _PartyMode.GameData.RuleChosen ? "TR_BUTTON_START_DUEL" : "TR_BUTTON_SPIN";
if (_PartyMode.IsFinished)
{
_SelectElement(_Buttons[_ButtonExit]);
}
else
{
_SelectElement(_Buttons[_ButtonNextRound]);
}
}
private static float _EaseOutBack(float t)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1f;
return 1f + c3 * (float)Math.Pow(t - 1f, 3) + c1 * (float)Math.Pow(t - 1f, 2);
}
private string _GetName(CRoulettePlayer player)
{
return CBase.Profiles.GetPlayerName(player.ProfileId);
}
private string _Translate(string key)
{
return CBase.Language.Translate(key, PartyModeId);
}
private void _LayoutCards()
{
var wheel = _PartyMode.GameData.Wheel;
var box = _Statics[_StaticCardBox].Rect;
for (var i = 0; i < _Cards.Count; i++)
{
var card = _Cards[i];
var visible = !_PartyMode.IsFinished && i < wheel.Count;
card.Box.Visible = visible;
card.Name.Visible = visible;
card.Active.Visible = false;
if (!visible)
{
continue;
}
var angle = 2 * Math.PI * i / wheel.Count - Math.PI / 2;
var centerX = _WheelCenterX + _WheelRadiusX * (float)Math.Cos(angle);
var centerY = _WheelCenterY + _WheelRadiusY * (float)Math.Sin(angle);
card.Box.MaxRect = new SRectF(centerX - box.W / 2, centerY - box.H / 2, box.W, box.H, box.Z);
card.Active.MaxRect = new SRectF(centerX - box.W / 2 - _ActiveBorder, centerY - box.H / 2 - _ActiveBorder, box.W + 2 * _ActiveBorder,
box.H + 2 * _ActiveBorder, _Statics[_StaticCardActive].Rect.Z);
card.Name.X = centerX;
card.Name.Y = centerY - card.Name.H / 2;
card.Name.Text = CPartyModeRuleRoulette.GetRuleKey(wheel[i]);
}
}
private void _UpdateCards(int highlight)
{
var wheel = _PartyMode.GameData.Wheel;
for (var i = 0; i < _Cards.Count && i < wheel.Count; i++)
{
var card = _Cards[i];
if (!card.Box.Visible)
{
continue;
}
var used = _PartyMode.GameData.UsedInCycle.Contains(wheel[i]) && i != _SpinTarget;
var isResult = !_Spinning && i == _SpinTarget;
var alpha = used && i != highlight ? _UsedAlpha : 1f;
var boxColor = _Statics[_StaticCardBox].Color;
card.Box.Color = new SColorF(boxColor.R, boxColor.G, boxColor.B, boxColor.A * alpha);
var nameColor = isResult ? _Texts[_TextCardNameResult].Color : _Texts[_TextCardName].Color;
card.Name.Color = new SColorF(nameColor.R, nameColor.G, nameColor.B, nameColor.A * alpha);
card.Active.Visible = i == highlight;
}
}
private void _StartSpin()
{
int target;
_PartyMode.SpinWheel(out target);
if (target < 0)
{
return;
}
_SpinTarget = target;
var count = _PartyMode.GameData.Wheel.Count;
var steps = _SpinRounds * count + target;
_SpinSteps = new List<float>();
var time = 0f;
for (var i = 0; i < steps; i++)
{
var progress = (float)i / steps;
time += _SpinMinStep + (_SpinMaxStep - _SpinMinStep) * progress * progress;
_SpinSteps.Add(time);
}
_Spinning = true;
_SpinTimer.Reset();
_SpinTimer.Start();
_UpdateCenter();
_Buttons[_ButtonNextRound].Visible = false;
_Buttons[_ButtonBack].Visible = false;
_Buttons[_ButtonExit].Visible = true;
}
private void _UpdateSpin()
{
var elapsed = _SpinTimer.ElapsedMilliseconds;
var step = 0;
while (step < _SpinSteps.Count && _SpinSteps[step] <= elapsed)
{
step++;
}
var count = _PartyMode.GameData.Wheel.Count;
if (step >= _SpinSteps.Count && elapsed >= _SpinSteps[_SpinSteps.Count - 1] + 300f)
{
_Spinning = false;
_UpdateCards(_SpinTarget);
_UpdateCenter();
_UpdateButtons();
_AnimTimer.Reset();
_AnimTimer.Start();
return;
}
_UpdateCards(step % count);
}
private void _UpdateTable()
{
var ranking = _PartyMode.GetRanking();
for (var i = 0; i < _Rows.Count; i++)
{
var row = _Rows[i];
var visible = i < ranking.Count;
row.Avatar.Visible = visible;
row.Pos.Visible = visible;
row.Name.Visible = visible;
row.Points.Visible = visible;
row.Highlight.Visible = visible && i == 0 && ranking[0].TablePoints > 0;
if (!visible)
{
continue;
}
var player = ranking[i];
row.Avatar.Texture = CBase.Profiles.GetAvatar(player.ProfileId);
row.Pos.Text = (i + 1) + ".";
row.Name.Text = _GetName(player);
row.Points.Text = player.TablePoints.ToString();
SColorF rankColor;
var color = _Texts[_TextRowName].Color;
if (_PartyMode.GameData.NumDuelsPlayed > 0 && i < _RankColors.Length && CBase.Themes.GetColor(_RankColors[i], PartyModeId, out rankColor))
{
color = rankColor;
}
row.Pos.Color = color;
row.Name.Color = color;
}
}
private void _UpdateCenter()
{
var finished = _PartyMode.IsFinished;
var duel = _PartyMode.CurrentDuel;
var showDuel = !finished && duel != null;
var showRule = showDuel && _PartyMode.GameData.RuleChosen && !_Spinning;
_Statics[_StaticAvatar2].Visible = showDuel;
_Texts[_TextName1].Visible = showDuel;
_Texts[_TextName2].Visible = showDuel;
_Texts[_TextVersus].Visible = showDuel;
_Texts[_TextDuel].Visible = showDuel;
_Texts[_TextRuleTitle].Visible = showRule;
_Texts[_TextRuleDescription].Visible = showRule;
_Texts[_TextRuleInfo].Visible = false;
_Texts[_TextLastResult].Visible = !finished && _PartyMode.GameData.LastDuel != null && !showRule && !_Spinning;
_Statics[_StaticAvatar1].MaxRect = _Avatar1Rect;
_Statics[_StaticAvatar1].Visible = showDuel || finished;
if (showDuel)
{
_Statics[_StaticAvatar1].Texture = CBase.Profiles.GetAvatar(duel.Players[0].ProfileId);
_Statics[_StaticAvatar2].Texture = CBase.Profiles.GetAvatar(duel.Players[1].ProfileId);
_Texts[_TextName1].Text = _GetName(duel.Players[0]);
_Texts[_TextName2].Text = _GetName(duel.Players[1]);
_Texts[_TextDuel].Text = _Translate("TR_SCREENMAIN_DUEL") + " " + (_PartyMode.GameData.DuelIndex + 1) + " / " + _PartyMode.GameData.Duels.Count;
}
if (showRule)
{
var key = CPartyModeRuleRoulette.GetRuleKey(duel.Rule);
_Texts[_TextRuleTitle].Text = key;
_Texts[_TextRuleDescription].Text = key + "_DESC";
if (duel.Rule == ERule.TargetScore)
{
_Texts[_TextRuleInfo].Visible = true;
_Texts[_TextRuleInfo].Text = _Translate("TR_SCREENMAIN_TARGET") + " " + duel.TargetScore.ToString("N0");
}
else if (duel.Rule == ERule.UnderdogChoice && duel.Chooser != null)
{
_Texts[_TextRuleInfo].Visible = true;
_Texts[_TextRuleInfo].Text = _Translate("TR_SCREENMAIN_CHOOSER").Replace("%s", _GetName(duel.Chooser));
}
else if (duel.Rule == ERule.JokerRain)
{
_Texts[_TextRuleInfo].Visible = true;
_Texts[_TextRuleInfo].Text = _Translate("TR_SCREENMAIN_JOKERS") + " " + _GetName(duel.Players[0]) + " " + duel.Players[0].Jokers + " · " +
_GetName(duel.Players[1]) + " " + duel.Players[1].Jokers;
}
}
var last = _PartyMode.GameData.LastDuel;
if (last != null)
{
_Texts[_TextLastResult].Text = _Translate("TR_SCREENMAIN_LAST_DUEL") + " " + _GetName(last.Players[0]) + " " + last.Scores[0].ToString("N0") + " (" +
_FormatAward(last.Awarded[0]) + ") · " + _GetName(last.Players[1]) + " " + last.Scores[1].ToString("N0") + " (" +
_FormatAward(last.Awarded[1]) + ")";
}
_Texts[_TextChampionLabel].Visible = finished;
_Texts[_TextChampion].Visible = finished;
_ParticleEffects[_ParticleEffectChampion].Visible = finished;
if (finished)
{
var champion = _PartyMode.GetRanking()[0];
_Texts[_TextChampion].Text = _GetName(champion);
_Statics[_StaticAvatar1].Texture = CBase.Profiles.GetAvatar(champion.ProfileId);
_Statics[_StaticAvatar1].X = _WheelCenterX - _Avatar1Rect.W / 2;
_Statics[_StaticAvatar1].Y = _Avatar1Rect.Y + 80f;
}
}
private static string _FormatAward(int award)
{
return award > 0 ? "+" + award : award.ToString();
}
private void _UpdateAnimations()
{
var time = _AnimTimer.ElapsedMilliseconds;
var pop = time < _PopDuration ? 0.3f + 0.7f * _EaseOutBack(time / _PopDuration) : 1f;
_Texts[_TextLastResult].Font.Height = _LastResultHeight * (_ShowNewResult ? pop : 1f);
if (_PartyMode.IsFinished)
{
var scale = time < _PopDuration ? pop : 1f + 0.03f * (float)Math.Sin((time - _PopDuration) / 1000.0 * Math.PI * 1.4);
_Texts[_TextChampion].Font.Height = _ChampionHeight * scale;
}
}
}
}