Files
Vocaluxe/PartyModes/PartyModeTeamSurvival/CPartyScreenTeamSurvivalMain.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

560 lines
20 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.TeamSurvival
{
class CPlayerCard
{
public CSurvivalPlayer Player;
public CStatic Active;
public CStatic Box;
public CStatic Avatar;
public CText Name;
public CStatic[] Hearts;
public SRectF[] HeartRects;
}
public class CPartyScreenTeamSurvivalMain : CPartyScreenTeamSurvival
{
protected override int _ScreenVersion
{
get { return 1; }
}
private const string _TextPopupReallyExit = "TextPopupReallyExit";
private const string _TextTeam1 = "TextTeam1";
private const string _TextTeam2 = "TextTeam2";
private const string _TextPass = "TextPass";
private const string _TextCardName = "TextCardName";
private const string _TextDuelName1 = "TextDuelName1";
private const string _TextDuelName2 = "TextDuelName2";
private const string _TextVersus = "TextVersus";
private const string _TextRematch = "TextRematch";
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 _StaticCardBox = "StaticCardBox";
private const string _StaticCardActive = "StaticCardActive";
private const string _StaticCardAvatar = "StaticCardAvatar";
private const string _StaticHeart = "StaticHeart";
private const string _StaticDuelFrame1 = "StaticDuelFrame1";
private const string _StaticDuelFrame2 = "StaticDuelFrame2";
private const string _StaticDuelAvatar1 = "StaticDuelAvatar1";
private const string _StaticDuelAvatar2 = "StaticDuelAvatar2";
private const string _ParticleEffectChampion = "ParticleEffectChampion";
private const float _CardFirstY = 265f;
private const float _CardStep = 76f;
private const float _ActiveBorder = 4f;
private const float _HeartSize = 32f;
private const float _HeartSpace = 6f;
private const float _LostHeartAlpha = 0.18f;
private const float _OutAlpha = 0.35f;
private const float _SurvivorSize = 110f;
private const float _SurvivorY = 470f;
private const float _SurvivorSpace = 16f;
private const float _HeartLossDelay = 400f;
private const float _HeartLossDuration = 800f;
private const float _ChampionPopDuration = 700f;
private readonly float[] _CardX = new float[] { 60f, 1340f };
private bool _ExitPopupVisible;
private List<CPlayerCard> _Cards;
private List<CStatic> _Survivors;
private readonly Stopwatch _AnimTimer = new Stopwatch();
private CPlayerCard _LosingCard;
private int _LostHeart;
private bool _LosingCardOut;
private float _ChampionHeight;
public override void Init()
{
base.Init();
_ThemeTexts = new string[]
{
_TextPopupReallyExit, _TextTeam1, _TextTeam2, _TextPass, _TextCardName, _TextDuelName1, _TextDuelName2, _TextVersus, _TextRematch, _TextChampionLabel,
_TextChampion
};
_ThemeButtons = new string[] { _ButtonNextRound, _ButtonBack, _ButtonExit, _ButtonPopupYes, _ButtonPopupNo };
_ThemeStatics = new string[]
{
_StaticPopupBG, _StaticCardBox, _StaticCardActive, _StaticCardAvatar, _StaticHeart, _StaticDuelFrame1, _StaticDuelFrame2, _StaticDuelAvatar1,
_StaticDuelAvatar2
};
_ThemeParticleEffects = new string[] { _ParticleEffectChampion };
}
public override void LoadTheme(string xmlPath)
{
base.LoadTheme(xmlPath);
_Cards = new List<CPlayerCard>();
for (var i = 0; i < CPartyModeTeamSurvival.TeamCount * CPartyModeTeamSurvival.MaxPlayersInTeam; i++)
{
var card = new CPlayerCard
{
Active = GetNewStatic(_Statics[_StaticCardActive]),
Box = GetNewStatic(_Statics[_StaticCardBox]),
Avatar = GetNewStatic(_Statics[_StaticCardAvatar]),
Name = GetNewText(_Texts[_TextCardName]),
Hearts = new CStatic[CPartyModeTeamSurvival.MaxHearts],
HeartRects = new SRectF[CPartyModeTeamSurvival.MaxHearts]
};
card.Avatar.Aspect = EAspect.Crop;
_AddStatic(card.Active);
_AddStatic(card.Box);
_AddStatic(card.Avatar);
_AddText(card.Name);
for (var h = 0; h < CPartyModeTeamSurvival.MaxHearts; h++)
{
card.Hearts[h] = GetNewStatic(_Statics[_StaticHeart]);
_AddStatic(card.Hearts[h]);
}
_Cards.Add(card);
}
_Survivors = new List<CStatic>();
for (var i = 0; i < CPartyModeTeamSurvival.MaxPlayersInTeam; i++)
{
var survivor = GetNewStatic(_Statics[_StaticCardAvatar]);
survivor.Aspect = EAspect.Crop;
_AddStatic(survivor);
_Survivors.Add(survivor);
}
_Statics[_StaticCardBox].Visible = false;
_Statics[_StaticCardActive].Visible = false;
_Statics[_StaticCardAvatar].Visible = false;
_Statics[_StaticHeart].Visible = false;
_Texts[_TextCardName].Visible = false;
_Statics[_StaticDuelAvatar1].Aspect = EAspect.Crop;
_Statics[_StaticDuelAvatar2].Aspect = EAspect.Crop;
_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();
_BuildCards();
_UpdateCards();
_UpdateDuel();
_LosingCard = null;
var decided = _PartyMode.GameData.LastDecidedDuel;
_PartyMode.GameData.LastDecidedDuel = null;
if (decided != null)
{
_LosingCard = _FindCard(decided.Players[decided.Loser]);
_LostHeart = decided.Players[decided.Loser].Hearts;
_LosingCardOut = decided.Players[decided.Loser].IsOut;
}
_AnimTimer.Reset();
_AnimTimer.Start();
_UpdateHeartLoss();
var started = _PartyMode.GameData.NumDuelsPlayed > 0;
_Buttons[_ButtonBack].Visible = !started;
_Buttons[_ButtonExit].Visible = started;
_Buttons[_ButtonNextRound].Visible = !_PartyMode.IsFinished;
_ShowPopup(false);
if (_PartyMode.IsFinished)
{
_SelectElement(_Buttons[_ButtonExit]);
}
else
{
_SelectElement(_Buttons[_ButtonNextRound]);
}
}
public override bool UpdateGame()
{
_UpdateHeartLoss();
if (_PartyMode.IsFinished)
{
_UpdateChampion();
}
return true;
}
private void _OnBack()
{
if (_ExitPopupVisible)
{
_ShowPopup(false);
}
else if (_PartyMode.GameData.NumDuelsPlayed == 0)
{
_PartyMode.Back();
}
else if (_PartyMode.IsFinished)
{
_EndParty();
}
else
{
_ShowPopup(true);
}
}
private void _OnSelect()
{
if (_ExitPopupVisible)
{
if (_Buttons[_ButtonPopupYes].Selected)
{
_EndParty();
}
if (_Buttons[_ButtonPopupNo].Selected)
{
_ShowPopup(false);
}
return;
}
if (_Buttons[_ButtonNextRound].Selected && _Buttons[_ButtonNextRound].Visible)
{
_PartyMode.Next();
}
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 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 static SColorF _WithAlpha(SColorF color, float alpha)
{
return new SColorF(color.R, color.G, color.B, color.A * alpha);
}
private string _GetTeamName(int team)
{
return CBase.Language.Translate("TR_TEAM", PartyModeId) + " " + (team + 1);
}
private CPlayerCard _FindCard(CSurvivalPlayer player)
{
foreach (var card in _Cards)
{
if (card.Player == player)
{
return card;
}
}
return null;
}
private void _BuildCards()
{
var cardNr = 0;
for (var team = 0; team < CPartyModeTeamSurvival.TeamCount; team++)
{
var players = _PartyMode.GetTeam(team);
for (var i = 0; i < players.Count && cardNr < _Cards.Count; i++)
{
var card = _Cards[cardNr++];
card.Player = players[i];
var boxTemplate = _Statics[_StaticCardBox].Rect;
var box = new SRectF(_CardX[team], _CardFirstY + i * _CardStep, boxTemplate.W, boxTemplate.H, boxTemplate.Z);
card.Box.MaxRect = box;
card.Active.MaxRect = new SRectF(box.X - _ActiveBorder, box.Y - _ActiveBorder, box.W + 2 * _ActiveBorder, box.H + 2 * _ActiveBorder,
_Statics[_StaticCardActive].Rect.Z);
var avatarTemplate = _Statics[_StaticCardAvatar].Rect;
card.Avatar.MaxRect = new SRectF(box.X + 6f, box.Y + (box.H - avatarTemplate.H) / 2, avatarTemplate.W, avatarTemplate.H, avatarTemplate.Z);
card.Avatar.Texture = CBase.Profiles.GetAvatar(card.Player.ProfileId);
card.Name.Text = CBase.Profiles.GetPlayerName(card.Player.ProfileId);
card.Name.X = box.X + avatarTemplate.W + 18f;
card.Name.Y = box.Y + (box.H - card.Name.H) / 2;
card.Name.W = box.W - avatarTemplate.W - 30f - CPartyModeTeamSurvival.MaxHearts * (_HeartSize + _HeartSpace);
var heartsX = box.X + box.W - 10f - card.Player.StartHearts * (_HeartSize + _HeartSpace);
for (var h = 0; h < card.Hearts.Length; h++)
{
card.HeartRects[h] = new SRectF(heartsX + h * (_HeartSize + _HeartSpace), box.Y + (box.H - _HeartSize) / 2, _HeartSize, _HeartSize,
_Statics[_StaticHeart].Rect.Z);
card.Hearts[h].MaxRect = card.HeartRects[h];
}
}
}
for (var i = cardNr; i < _Cards.Count; i++)
{
_Cards[i].Player = null;
}
}
private void _UpdateCards()
{
var duel = _PartyMode.CurrentDuel;
foreach (var card in _Cards)
{
var visible = card.Player != null;
card.Box.Visible = visible;
card.Avatar.Visible = visible;
card.Name.Visible = visible;
card.Active.Visible = visible && duel != null && (duel.Players[0] == card.Player || duel.Players[1] == card.Player);
for (var h = 0; h < card.Hearts.Length; h++)
{
card.Hearts[h].Visible = visible && h < card.Player.StartHearts;
}
if (visible)
{
_SetCardAlpha(card, card.Player.IsOut ? _OutAlpha : 1f);
_SetHeartAlphas(card, card.Player.Hearts);
}
}
for (var team = 0; team < CPartyModeTeamSurvival.TeamCount; team++)
{
var text = _Texts[team == 0 ? _TextTeam1 : _TextTeam2];
text.Text = _GetTeamName(team);
text.Color = CBase.Themes.GetPlayerColor(team + 1);
}
}
private void _SetCardAlpha(CPlayerCard card, float alpha)
{
card.Box.Color = _WithAlpha(_Statics[_StaticCardBox].Color, alpha);
card.Avatar.Color = _WithAlpha(_Statics[_StaticCardAvatar].Color, alpha);
card.Name.Color = _WithAlpha(_Texts[_TextCardName].Color, alpha);
}
private void _SetHeartAlphas(CPlayerCard card, int hearts)
{
for (var h = 0; h < card.Hearts.Length; h++)
{
card.Hearts[h].MaxRect = card.HeartRects[h];
card.Hearts[h].Color = _WithAlpha(_Statics[_StaticHeart].Color, h < hearts ? 1f : _LostHeartAlpha);
}
}
private void _UpdateHeartLoss()
{
if (_LosingCard == null || _LostHeart < 0 || _LostHeart >= _LosingCard.Hearts.Length)
{
return;
}
var progress = Math.Max(0f, Math.Min(1f, (_AnimTimer.ElapsedMilliseconds - _HeartLossDelay) / _HeartLossDuration));
var heart = _LosingCard.Hearts[_LostHeart];
var rect = _LosingCard.HeartRects[_LostHeart];
var scale = 1f + progress;
var size = rect.W * scale;
heart.MaxRect = new SRectF(rect.X + (rect.W - size) / 2, rect.Y + (rect.H - size) / 2, size, size, rect.Z);
heart.Color = _WithAlpha(_Statics[_StaticHeart].Color, 1f - (1f - _LostHeartAlpha) * progress);
if (_LosingCardOut)
{
_SetCardAlpha(_LosingCard, 1f - (1f - _OutAlpha) * progress);
}
if (progress >= 1f)
{
heart.MaxRect = rect;
_LosingCard = null;
}
}
private void _UpdateDuel()
{
var finished = _PartyMode.IsFinished;
var duel = _PartyMode.CurrentDuel;
var showDuel = !finished && duel != null;
_Statics[_StaticDuelFrame1].Visible = showDuel;
_Statics[_StaticDuelFrame2].Visible = showDuel;
_Statics[_StaticDuelAvatar1].Visible = showDuel;
_Statics[_StaticDuelAvatar2].Visible = showDuel;
_Texts[_TextDuelName1].Visible = showDuel;
_Texts[_TextDuelName2].Visible = showDuel;
_Texts[_TextVersus].Visible = showDuel;
_Texts[_TextPass].Visible = showDuel;
_Texts[_TextRematch].Visible = showDuel && _PartyMode.GameData.IsRematch;
if (showDuel)
{
_Statics[_StaticDuelFrame1].Color = CBase.Themes.GetPlayerColor(1);
_Statics[_StaticDuelFrame2].Color = CBase.Themes.GetPlayerColor(2);
_Statics[_StaticDuelAvatar1].Texture = CBase.Profiles.GetAvatar(duel.Players[0].ProfileId);
_Statics[_StaticDuelAvatar2].Texture = CBase.Profiles.GetAvatar(duel.Players[1].ProfileId);
_Texts[_TextDuelName1].Text = CBase.Profiles.GetPlayerName(duel.Players[0].ProfileId);
_Texts[_TextDuelName2].Text = CBase.Profiles.GetPlayerName(duel.Players[1].ProfileId);
_Texts[_TextPass].Text = CBase.Language.Translate("TR_SCREENMAIN_PASS", PartyModeId) + " " + _PartyMode.GameData.PassNr + " · " +
CBase.Language.Translate("TR_SCREENMAIN_DUEL", PartyModeId) + " " + (_PartyMode.GameData.DuelIndex + 1) + " / " +
_PartyMode.GameData.PassDuels.Count;
}
_Texts[_TextChampionLabel].Visible = finished;
_Texts[_TextChampion].Visible = finished;
_ParticleEffects[_ParticleEffectChampion].Visible = finished;
var survivors = new List<CSurvivalPlayer>();
if (finished)
{
_Texts[_TextChampion].Text = _GetTeamName(_PartyMode.GameData.WinnerTeam);
foreach (var player in _PartyMode.GetTeam(_PartyMode.GameData.WinnerTeam))
{
if (!player.IsOut)
{
survivors.Add(player);
}
}
}
var x = (CBase.Settings.GetRenderW() - survivors.Count * _SurvivorSize - (survivors.Count - 1) * _SurvivorSpace) / 2;
for (var i = 0; i < _Survivors.Count; i++)
{
_Survivors[i].Visible = i < survivors.Count;
if (i >= survivors.Count)
{
continue;
}
_Survivors[i].Texture = CBase.Profiles.GetAvatar(survivors[i].ProfileId);
_Survivors[i].MaxRect = new SRectF(x, _SurvivorY, _SurvivorSize, _SurvivorSize, _Statics[_StaticCardAvatar].Rect.Z);
x += _SurvivorSize + _SurvivorSpace;
}
}
private void _UpdateChampion()
{
var time = _AnimTimer.ElapsedMilliseconds;
float scale;
if (time < _ChampionPopDuration)
{
scale = 0.3f + 0.7f * _EaseOutBack(time / _ChampionPopDuration);
}
else
{
scale = 1f + 0.03f * (float)Math.Sin((time - _ChampionPopDuration) / 1000.0 * Math.PI * 1.4);
}
_Texts[_TextChampion].Font.Height = _ChampionHeight * scale;
}
}
}