diff --git a/scripts/Rhythia.cs b/scripts/Rhythia.cs index 20eb553..d78fc3b 100644 --- a/scripts/Rhythia.cs +++ b/scripts/Rhythia.cs @@ -212,7 +212,7 @@ public override void _Notification(int what) else if (what == NotificationApplicationFocusIn) { var settings = SettingsManager.Instance.Settings; - Engine.MaxFps = settings.UnlockFPS ? 0 : settings.FPS; + Engine.MaxFps = settings.LockFPS ? settings.FPS : 0; } } } diff --git a/scripts/SoundManager.cs b/scripts/SoundManager.cs index 7bf7cbb..6dc57a2 100644 --- a/scripts/SoundManager.cs +++ b/scripts/SoundManager.cs @@ -6,12 +6,20 @@ public partial class SoundManager : Node, ISkinnable { + public enum PlaybackScope + { + Silent, + Preview, + GameplayResults, + } + public static SoundManager Instance; public static AudioStreamPlayer HitSound; public static AudioStreamPlayer MissSound; public static AudioStreamPlayer FailSound; public static AudioStreamPlayer Song; + public static AudioStreamPlayer MenuMusic; public Action JukeboxPlayed; @@ -22,9 +30,12 @@ public partial class SoundManager : Node, ISkinnable public static bool JukeboxPaused = false; public static ulong LastRewind = 0; public static Map Map; + public static PlaybackScope Scope = PlaybackScope.Silent; private static bool volumePopupShown = false; private static ulong lastVolumeChange = 0; + private static bool? jeeping = null; // we're jeeping (last state of a song) + private static bool menuMusicPausedByUser = false; public override void _Ready() { @@ -34,13 +45,16 @@ public override void _Ready() MissSound = new(); FailSound = new(); Song = new(); + MenuMusic = new(); HitSound.MaxPolyphony = 16; + MissSound.MaxPolyphony = 16; AddChild(HitSound); AddChild(MissSound); AddChild(FailSound); AddChild(Song); + AddChild(MenuMusic); SkinManager.Instance.Loaded += UpdateSkin; @@ -48,6 +62,12 @@ public override void _Ready() Song.Finished += () => { + if (isScopedPlayback()) + { + StopScopedSession(); + return; + } + switch (SceneManager.Scene.Name) { case "SceneMenu": @@ -67,15 +87,7 @@ public override void _Ready() SettingsManager.Instance.Loaded += UpdateVolume; Lobby.Instance.SpeedChanged += (speed) => { SoundManager.Song.PitchScale = (float)speed; }; - MapManager.Selected.ValueChanged += (_, selected) => - { - var map = selected.Value; - - if (Map == null || Map.Name != map.Name) - { - PlayJukebox(map); - } - }; + MapManager.Selected.ValueChanged += (_, _) => RefreshMenuMusicPlayback(); MapManager.MapDeleted += (map) => { @@ -86,6 +98,12 @@ public override void _Ready() return; } + if (isScopedPlayback()) + { + StopScopedSession(); + return; + } + if (JukeboxQueue.Length == 0) { Song.Stop(); @@ -108,19 +126,30 @@ static void start() { PlayJukebox(new Random().Next(0, JukeboxQueue.Length)); } + else + { + StopScopedSession(); + } } if (MapManager.Initialized) { start(); + printSongPlaybackState(); return; } MapManager.MapsInitialized += _ => start(); + + RefreshMenuMusicPlayback(); + printSongPlaybackState(); } public override void _Process(double delta) { + RefreshMenuMusicPlayback(); + printSongPlaybackState(); + if (volumePopupShown && Time.GetTicksMsec() - lastVolumeChange >= 1000) { volumePopupShown = false; @@ -167,6 +196,20 @@ public override void _UnhandledInput(InputEvent @event) public static void PlayJukebox(Map map, bool setRichPresence = true) { + if (map == null) + { + return; + } + + if (isScopedPlayback()) + { + StartMapSelectionPlayback(map, setRichPresence); + return; + } + + MenuMusic?.Stop(); + menuMusicPausedByUser = false; + Map = map; if (map.AudioBuffer == null) @@ -212,6 +255,117 @@ public static void PlayJukebox(int index = -1, bool setRichPresence = true) PlayJukebox(map, setRichPresence); } + public static void StartMapSelectionPlayback(Map map, bool setRichPresence = true) + { + if (map == null) + { + return; + } + + Song.Stop(); + Song.StreamPaused = false; + Song.PitchScale = (float)Lobby.Speed; + MenuMusic?.Stop(); + menuMusicPausedByUser = false; + + Map = map; + Scope = PlaybackScope.Preview; + + if (MapManager.Maps != null) + { + int mapIndex = MapManager.Maps.FindIndex(x => x.Id == map.Id); + if (mapIndex >= 0) + { + JukeboxIndex = mapIndex; + } + } + + Song.Stream = Util.Audio.LoadFromFile($"{MapUtil.MapsCacheFolder}/{map.Name}/audio.{map.AudioExt}"); + Song.Play(0); + + Instance.JukeboxPlayed?.Invoke(map); + + if (setRichPresence) + { + Discord.Client.UpdateState($"Listening to {map.PrettyTitle}"); + } + } + + public static void BeginGameplayScope(Map map) + { + if (!isScopedPlayback()) + { + return; + } + + Map = map; + Scope = PlaybackScope.GameplayResults; + MenuMusic?.Stop(); + } + + public static void StopScopedSession() + { + Song.Stop(); + Song.StreamPaused = false; + Map = null; + Scope = PlaybackScope.Silent; + Instance.JukeboxEmpty?.Invoke(); + + RefreshMenuMusicPlayback(); + } + + public static bool IsJukeboxPaused() + { + if (Song != null && Song.StreamPaused) + { + return true; + } + + return menuMusicPausedByUser; + } + + public static bool ToggleJukeboxPause() + { + if (Song != null && (Song.Playing || Song.StreamPaused)) + { + Song.StreamPaused = !Song.StreamPaused; + return Song.StreamPaused; + } + + if (MenuMusic == null || MenuMusic.Stream == null) + { + return false; + } + + if (!shouldPlayMenuMusic() && !menuMusicPausedByUser) + { + return false; + } + + menuMusicPausedByUser = !menuMusicPausedByUser; + + if (menuMusicPausedByUser) + { + MenuMusic.StreamPaused = true; + } + else + { + if (!MenuMusic.Playing) + { + MenuMusic.Play(); + } + + MenuMusic.StreamPaused = false; + } + + return menuMusicPausedByUser; + } + + private static bool isScopedPlayback() + { + return !SettingsManager.Instance.Settings.AutoplayJukebox.Value; + } + public static float ComputeVolumeDb(float volume, float master, float range) { if (volume <= 0 || master <= 0) return float.NegativeInfinity; @@ -223,10 +377,32 @@ public static void UpdateVolume() var settings = SettingsManager.Instance.Settings; Song.VolumeDb = ComputeVolumeDb(settings.VolumeMusic.Value, settings.VolumeMaster.Value, 70); - HitSound.VolumeDb = ComputeVolumeDb(settings.VolumeSFX.Value, settings.VolumeMaster.Value, 80); + MenuMusic.VolumeDb = ComputeVolumeDb(settings.VolumeMenuMusic.Value, settings.VolumeMaster.Value, 70); + HitSound.VolumeDb = ComputeVolumeDb(settings.VolumeHitSound.Value, settings.VolumeMaster.Value, 80); + MissSound.VolumeDb = ComputeVolumeDb(settings.VolumeMissSound.Value, settings.VolumeMaster.Value, 80); FailSound.VolumeDb = ComputeVolumeDb(settings.VolumeSFX.Value, settings.VolumeMaster.Value, 80); } + public static void PlayHitSound() + { + if (!isSoundEffectEnabled(SettingsManager.Instance?.Settings.EnableHitSound, HitSound)) + { + return; + } + + HitSound.Play(); + } + + public static void PlayMissSound() + { + if (!isSoundEffectEnabled(SettingsManager.Instance?.Settings.EnableMissSound, MissSound)) + { + return; + } + + MissSound.Play(); + } + public static void UpdateJukeboxQueue() { JukeboxQueue = [.. MapManager.Maps.Select(x => x.Id)]; @@ -235,6 +411,87 @@ public static void UpdateJukeboxQueue() public void UpdateSkin(SkinProfile skin) { HitSound.Stream = Util.Audio.LoadStream(skin.HitSoundBuffer); + MissSound.Stream = Util.Audio.LoadStream(skin.MissSoundBuffer); FailSound.Stream = Util.Audio.LoadStream(skin.FailSoundBuffer); + MenuMusic.Stream = Util.Audio.LoadStream(skin.MenuMusicBuffer); + + RefreshMenuMusicPlayback(); + } + + public static void RefreshMenuMusicPlayback() + { + if (MenuMusic == null) + { + return; + } + + if (menuMusicPausedByUser) + { + if (MenuMusic.Playing && !MenuMusic.StreamPaused) + { + MenuMusic.StreamPaused = true; + } + + return; + } + + if (shouldPlayMenuMusic()) + { + if (!MenuMusic.Playing) + { + MenuMusic.Play(); + } + + if (MenuMusic.StreamPaused) + { + MenuMusic.StreamPaused = false; + } + } + else if (MenuMusic.Playing) + { + MenuMusic.Stop(); + } + } + + private static bool shouldPlayMenuMusic() + { + if (!SettingsManager.Instance.Settings.EnableMenuMusic.Value) + { + return false; + } + + if (MenuMusic.Stream == null) + { + return false; + } + + if (SceneManager.Scene is not MainMenu) + { + return false; + } + + if (Song != null && Song.Playing) + { + return false; + } + + return true; + } + + private static bool isSoundEffectEnabled(SettingsItem setting, AudioStreamPlayer player) + { + return setting != null && setting.Value && player?.Stream != null; + } + + private static void printSongPlaybackState() + { + bool isSongPlaying = Song != null && Song.Playing; + + if (jeeping == isSongPlaying) // vroom vroom jeep + { + return; + } + + jeeping = isSongPlaying; //jeeps go beep beep } } diff --git a/scripts/database/settings/SettingsProfile.cs b/scripts/database/settings/SettingsProfile.cs index 2a51da9..fcd3d82 100644 --- a/scripts/database/settings/SettingsProfile.cs +++ b/scripts/database/settings/SettingsProfile.cs @@ -197,6 +197,12 @@ public partial class SettingsProfile [Order] public SettingsItem SimpleHUD { get; private set; } + /// + /// Toggles super minimal HUD + /// + [Order] + public SettingsItem SuperSimpleHUD { get; private set; } + /// /// Toggles a popup on a hit /// @@ -220,10 +226,10 @@ public partial class SettingsProfile public SettingsItem Fullscreen { get; private set; } /// - /// Unlocks maximum frames per second + /// Locks maximum frames per second /// [Order] - public SettingsItem UnlockFPS { get; private set; } + public SettingsItem LockFPS { get; private set; } /// /// Adjusts maximum frames per second @@ -253,18 +259,60 @@ public partial class SettingsProfile [Order] public SettingsItem VolumeSFX { get; private set; } + /// + /// Audio control for hit sound + /// + [Order] + public SettingsItem VolumeHitSound { get; private set; } + + /// + /// Audio control for miss sound + /// + [Order] + public SettingsItem VolumeMissSound { get; private set; } + + /// + /// Audio control for menu music + /// + [Order] + public SettingsItem VolumeMenuMusic { get; private set; } + /// /// Toggles hit sound to always play /// [Order] public SettingsItem AlwaysPlayHitSound { get; private set; } + /// + /// Enables hit sound playback + /// + [Order] + public SettingsItem EnableHitSound { get; private set; } + + /// + /// Enables miss sound playback + /// + [Order] + public SettingsItem EnableMissSound { get; private set; } + + /// + /// Enables menu music playback + /// + [Order] + public SettingsItem EnableMenuMusic { get; private set; } + /// /// Automatically plays the jukebox on start /// [Order] public SettingsItem AutoplayJukebox { get; private set; } + /// + /// Adjusts the local audio offset in milliseconds + /// + [Order] + public SettingsItem LocalOffset { get; private set; } + #endregion #region Other @@ -694,6 +742,14 @@ public SettingsProfile() Section = SettingsSection.Visual, }; + SuperSimpleHUD = new(false) + { + Id = "SuperSimpleHUD", + Title = "Super Simple HUD", + Description = "Hides health bar, song duration, and song name", + Section = SettingsSection.Visual, + }; + HitPopups = new(true) { Id = "HitPopups", @@ -722,13 +778,13 @@ public SettingsProfile() : DisplayServer.WindowMode.Windowed) }; - UnlockFPS = new(true) + LockFPS = new(true) { - Id = "UnlockFPS", - Title = "Unlock FPS", - Description = "Unlocks maximum frames per second", + Id = "LockFPS", + Title = "Lock FPS", + Description = "Locks maximum frames per second", Section = SettingsSection.Video, - UpdateAction = (value, _) => Engine.MaxFps = UnlockFPS ? 0 : FPS + UpdateAction = (value, _) => Engine.MaxFps = value ? FPS.Value : 0 }; FPS = new(240) @@ -743,7 +799,7 @@ public SettingsProfile() MinValue = 60, MaxValue = 540, }, - UpdateAction = (value, _) => Engine.MaxFps = UnlockFPS ? 0 : FPS + UpdateAction = (value, _) => Engine.MaxFps = LockFPS.Value ? value : 0 }; #endregion @@ -758,6 +814,20 @@ public SettingsProfile() Section = SettingsSection.Audio, }; + LocalOffset = new(0) + { + Id = "LocalOffset", + Title = "Local Offset", + Description = "Adjusts audio offset in milliseconds", + Section = SettingsSection.Audio, + Slider = new() + { + Step = 1, + MinValue = -500, + MaxValue = 500 + } + }; + AlwaysPlayHitSound = new(false) { Id = "AlwaysPlayHitSound", @@ -766,6 +836,37 @@ public SettingsProfile() Section = SettingsSection.Audio, }; + EnableHitSound = new(true) + { + Id = "EnableHitSound", + Title = "Enable Hit Sound", + Description = "Enables hit sound playback", + Section = SettingsSection.Audio, + }; + + EnableMissSound = new(true) + { + Id = "EnableMissSound", + Title = "Enable Miss Sound", + Description = "Enables miss sound playback", + Section = SettingsSection.Audio, + }; + + EnableMenuMusic = new(true) + { + Id = "EnableMenuMusic", + Title = "Enable Menu Music", + Description = "Enables menu music playback when the menu is quiet", + Section = SettingsSection.Audio, + UpdateAction = (_, init) => + { + if (!init) + { + SoundManager.RefreshMenuMusicPlayback(); + } + } + }; + VolumeMaster = new(50) { Id = "VolumeMaster", @@ -800,7 +901,52 @@ public SettingsProfile() { Id = "VolumeSFX", Title = "SFX Volume", - Description = "Audio control for sound effects", + Description = "Audio control for other sound effects", + Section = SettingsSection.Audio, + UpdateAction = (_, init) => { if (!init) { SoundManager.UpdateVolume(); } }, + Slider = new() + { + Step = 1, + MinValue = 0, + MaxValue = 100 + } + }; + + VolumeHitSound = new(50) + { + Id = "VolumeHitSound", + Title = "Hit Sound Volume", + Description = "Audio control for hit sound", + Section = SettingsSection.Audio, + UpdateAction = (_, init) => { if (!init) { SoundManager.UpdateVolume(); } }, + Slider = new() + { + Step = 1, + MinValue = 0, + MaxValue = 100 + } + }; + + VolumeMissSound = new(50) + { + Id = "VolumeMissSound", + Title = "Miss Sound Volume", + Description = "Audio control for miss sound", + Section = SettingsSection.Audio, + UpdateAction = (_, init) => { if (!init) { SoundManager.UpdateVolume(); } }, + Slider = new() + { + Step = 1, + MinValue = 0, + MaxValue = 100 + } + }; + + VolumeMenuMusic = new(50) + { + Id = "VolumeMenuMusic", + Title = "Menu Music Volume", + Description = "Audio control for menu music", Section = SettingsSection.Audio, UpdateAction = (_, init) => { if (!init) { SoundManager.UpdateVolume(); } }, Slider = new() diff --git a/scripts/map/Leaderboard.cs b/scripts/map/Leaderboard.cs index 0450b4c..e3fe78e 100644 --- a/scripts/map/Leaderboard.cs +++ b/scripts/map/Leaderboard.cs @@ -3,6 +3,7 @@ using System.Data.Common; using System.IO; using System.Security.Cryptography; +using System.Text; using Godot; public struct Leaderboard @@ -86,12 +87,14 @@ public void Save() foreach (Score score in Scores) { ulong offset = file.GetPosition(); + byte[] attemptIdBytes = Encoding.UTF8.GetBytes(score.AttemptID ?? string.Empty); + byte[] playerBytes = Encoding.UTF8.GetBytes(score.Player ?? string.Empty); file.Store32(0); // reserved for length - file.Store32((uint)score.AttemptID.Length); - file.StoreString(score.AttemptID); - file.Store32((uint)score.Player.Length); - file.StoreString(score.Player); + file.Store32((uint)attemptIdBytes.Length); + file.StoreBuffer(attemptIdBytes); + file.Store32((uint)playerBytes.Length); + file.StoreBuffer(playerBytes); file.Store8((byte)(score.Qualifies ? 1 : 0)); file.Store64(score.Value); file.StoreDouble(score.Accuracy); @@ -108,9 +111,10 @@ public void Save() } string json = Json.Stringify(modifiers); + byte[] modifiersBytes = Encoding.UTF8.GetBytes(json); - file.Store32((uint)json.Length); - file.StoreString(json); + file.Store32((uint)modifiersBytes.Length); + file.StoreBuffer(modifiersBytes); ulong end = file.GetPosition(); @@ -151,8 +155,14 @@ public Score(byte[] buffer) { FileParser FileBuffer = new(buffer); - AttemptID = FileBuffer.GetString((int)FileBuffer.GetUInt32()); - Player = FileBuffer.GetString((int)FileBuffer.GetUInt32()); + int attemptIdLength = (int)FileBuffer.GetUInt32(); + Console.WriteLine($"[Leaderboard] attemptId length={attemptIdLength}, remaining={FileBuffer.Length - FileBuffer.Pointer}"); + AttemptID = FileBuffer.GetString(attemptIdLength); + + int playerLength = (int)FileBuffer.GetUInt32(); + Console.WriteLine($"[Leaderboard] player length={playerLength}, remaining={FileBuffer.Length - FileBuffer.Pointer}"); + Player = FileBuffer.GetString(playerLength); + Qualifies = FileBuffer.GetBool(); Value = FileBuffer.GetUInt64(); Accuracy = FileBuffer.GetDouble(); @@ -162,7 +172,10 @@ public Score(byte[] buffer) Speed = FileBuffer.GetDouble(); Modifiers = []; - foreach (KeyValuePair entry in (Godot.Collections.Dictionary)Json.ParseString(FileBuffer.GetString((int)FileBuffer.GetUInt32()))) + int modifiersLength = (int)FileBuffer.GetUInt32(); + Console.WriteLine($"[Leaderboard] modifiers length={modifiersLength}, remaining={FileBuffer.Length - FileBuffer.Pointer}"); + + foreach (KeyValuePair entry in (Godot.Collections.Dictionary)Json.ParseString(FileBuffer.GetString(modifiersLength))) { Modifiers[entry.Key] = entry.Value; } diff --git a/scripts/map/MapCache.cs b/scripts/map/MapCache.cs index 58d7d44..d564020 100644 --- a/scripts/map/MapCache.cs +++ b/scripts/map/MapCache.cs @@ -252,7 +252,7 @@ public static void OrderAndSetMaps() if (map.Cover == Map.DefaultCover && File.Exists($"{path}/cover.png")) { byte[] coverBuffer = File.ReadAllBytes($"{path}/cover.png"); - if (coverBuffer?.Length <= 0) + if (coverBuffer == null || coverBuffer.Length == 0) { continue; } @@ -263,7 +263,10 @@ public static void OrderAndSetMaps() { Callable.From(() => { - map.Cover = ImageTexture.CreateFromImage(image); + if (MapManager.Maps.Contains(map)) + { + map.Cover = ImageTexture.CreateFromImage(image); + } }).CallDeferred(); } diff --git a/scripts/map/MapManager.cs b/scripts/map/MapManager.cs index ba6b1f8..590935f 100644 --- a/scripts/map/MapManager.cs +++ b/scripts/map/MapManager.cs @@ -96,7 +96,6 @@ public static void Delete(Map map) { try { - try { File.Delete(map.FilePath); @@ -105,15 +104,35 @@ public static void Delete(Map map) { if (File.Exists(map.FilePath)) { - Logger.Error("Unable to delete map"); + return; } } MapCache.RemoveMap(map); - Maps.Remove(map); + Maps.RemoveAll(x => x.Id == map.Id); + + SoundManager.UpdateJukeboxQueue(); + if (SoundManager.Map?.Name == map.Name) + { + if (SoundManager.JukeboxQueue.Length == 0) + { + SoundManager.Song.Stop(); + SoundManager.Map = null; + Callable.From(() => JukeboxPanel.Instance?.ClearMap()).CallDeferred(); + } + else + { + SoundManager.JukeboxIndex = Math.Clamp(SoundManager.JukeboxIndex, 0, SoundManager.JukeboxQueue.Length - 1); + Callable.From(() => SoundManager.PlayJukebox(SoundManager.JukeboxIndex)).CallDeferred(); + } + } - MapDeleted?.Invoke(map); + Callable.From(() => + { + _ = ToastNotification.Notify($"Deleted {map.PrettyTitle}!"); + }).CallDeferred(); + Callable.From(() => MapDeleted?.Invoke(map)).CallDeferred(); } catch (Exception e) { diff --git a/scripts/multiplayer/Lobby.cs b/scripts/multiplayer/Lobby.cs index 3293f29..d33b477 100644 --- a/scripts/multiplayer/Lobby.cs +++ b/scripts/multiplayer/Lobby.cs @@ -110,6 +110,8 @@ public static void SetMap(Map map) { Map = map; + SetStartFrom(0); + Instance.EmitSignal(SignalName.MapChanged, Map); } diff --git a/scripts/scenes/LegacyRunner.cs b/scripts/scenes/LegacyRunner.cs index 0081f09..ef5bf65 100644 --- a/scripts/scenes/LegacyRunner.cs +++ b/scripts/scenes/LegacyRunner.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; @@ -6,1555 +6,1596 @@ public partial class LegacyRunner : BaseScene { - private static SettingsProfile settings; - - private static Node node; - private static readonly PackedScene hit_feedback = GD.Load("res://prefabs/hit_popup.tscn"); - private static readonly PackedScene miss_feedback = GD.Load("res://prefabs/miss_icon.tscn"); - private static readonly PackedScene modifier_icon = GD.Load("res://prefabs/modifier.tscn"); - - public static Camera3D Camera; - - private static Panel menu; - //private static Label fpsCounter; - private static Label3D titleLabel; - private static Label3D comboLabel; - private static Label3D speedLabel; - private static Label3D skipLabel; - private static Label3D progressLabel; - //private static TextureRect jesus; - private static MeshInstance3D cursor; - private static MeshInstance3D grid; - private static MeshInstance3D videoQuad; - private static MultiMeshInstance3D notesMultimesh; - private static MultiMeshInstance3D cursorTrailMultimesh; - private static TextureRect healthTexture; - private static TextureRect progressBarTexture; - private static SubViewport panelLeft; - private static SubViewport panelRight; - //private static AudioStreamPlayer bell; - private static Panel replayViewer; - private static TextureButton replayViewerPause; - private static Label replayViewerLabel; - private static HSlider replayViewerSeek; - private static Label accuracyLabel; - private static Label hitsLabel; - private static Label missesLabel; - private static Label sumLabel; - private static Label simpleMissesLabel; - private static Label scoreLabel; - private static Label multiplierLabel; - private static Panel multiplierProgressPanel; - private static ShaderMaterial multiplierProgressMaterial; - private static float multiplierProgress = 0; // more efficient than spamming material.GetShaderParameter() - private static Color multiplierColour = Color.Color8(255, 255, 255); - private static VideoStreamPlayer video; - private static Tween hitTween; - private static Tween missTween; - private static bool stopQueued = false; - private static int hitPopups = 0; - private static int missPopups = 0; - private static bool replayViewerSeekHovered = false; - private static bool leftMouseButtonDown = false; - - private static Panel pauseOverlay; - private static bool pauseShown = false; - private static Panel quitOverlay; - private static ColorRect quitProgressBar; - private static float quitHoldTime = 0; - private static bool rKeyHeld = false; - private static float quitHoldDuration = 0.55f; - - private double lastFrame = Time.GetTicksUsec(); // delta arg unreliable.. - //private double lastSecond = Time.GetTicksUsec(); // better framerate calculation - private List> lastCursorPositions = []; // trail - //private int frameCount = 0; - private float skipLabelAlpha = 0; - private float targetSkipLabelAlpha = 0; - - public static bool Playing = false; - public static ulong Started = 0; - public static bool MenuShown = false; - public static bool SettingsShown = false; - public static bool ReplayViewerShown = false; - public static int ToProcess = 0; - public static List ProcessNotes = []; - public static Attempt CurrentAttempt = new(); - public static double MapLength; - //public static Tween JesusTween; - public static MeshInstance3D[] Cursors; - - public struct Attempt - { - private SettingsProfile settings; - private LegacyRunner runner; - - public string ID = ""; - public bool Stopped = false; - public bool IsReplay = false; - public Replay[] Replays; // when reading replays - public float LongestReplayLength = 0; - public List ReplayFrames = []; // when writing replays - public List ReplaySkips = []; - public ulong LastReplayFrame = 0; - public uint ReplayFrameCountOffset = 0; - public uint ReplayAttemptStatusOffset = 0; - public Godot.FileAccess ReplayFile; - public double Progress = 0; // ms - public Map Map = new(); - public double Speed = 1; - public double StartFrom = 0; - public ulong FirstNote = 0; - public Dictionary Mods; - public string[] Players = []; - public bool Alive = true; - public bool Skippable = false; - public bool Qualifies = true; - public uint Hits = 0; - public float[] HitsInfo = []; - public Color LastHitColour = SkinManager.Instance.Skin.NoteColors[^1]; - public uint Misses = 0; - public double DeathTime = -1; - public uint Sum = 0; - public uint Combo = 0; - public uint ComboMultiplier = 1; - public uint ComboMultiplierProgress = 0; - public uint ComboMultiplierIncrement = 0; - public double ModsMultiplier = 1; - public uint Score = 0; - public uint PassedNotes = 0; - public double Accuracy = 100; - public double Health = 100; - public double HealthStep = 15; - public Vector2 CursorPosition = Vector2.Zero; - public Vector2 RawCursorPosition = Vector2.Zero; - public double DistanceMM = 0; - - public Attempt(Map map, double speed, double startFrom, Dictionary mods, string[] players = null, Replay[] replays = null) - { - settings = SettingsManager.Instance.Settings; - - ID = $"{map.Name}_{OS.GetUniqueId()}_{Time.GetDatetimeStringFromUnixTime((long)Time.GetUnixTimeFromSystem())}".Replace(":", "_"); - Replays = replays; - IsReplay = Replays != null; - Map = map; - Speed = speed; - StartFrom = startFrom; - Players = players ?? []; - Progress = Speed * -1000 - settings.ApproachTime.Value * 1000 + StartFrom; - ComboMultiplierIncrement = Math.Max(2, (uint)Map.Notes.Length / 200); - Mods = []; - HitsInfo = IsReplay ? Replays[0].Notes : new float[Map.Notes.Length]; - - foreach (KeyValuePair mod in mods) - { - Mods[mod.Key] = mod.Value; - } - - if (StartFrom > 0) - { - Qualifies = false; - - foreach (Note note in Map.Notes) - { - if (note.Millisecond < StartFrom) - { - FirstNote = (ulong)note.Index + 1; - } - } - } - - if (!IsReplay && settings.RecordReplays && !Map.Ephemeral) - { - ReplayFile = Godot.FileAccess.Open($"{Constants.USER_FOLDER}/replays/{ID}.phxr", Godot.FileAccess.ModeFlags.Write); - ReplayFile.StoreString("phxr"); // sig - ReplayFile.Store8(1); // replay file version - - string mapFileName = Map.FilePath.GetFile().GetBaseName(); - - ReplayFile.StoreDouble(Speed); - ReplayFile.StoreDouble(StartFrom); - ReplayFile.StoreDouble(settings.ApproachRate); - ReplayFile.StoreDouble(settings.ApproachDistance); - ReplayFile.StoreDouble(settings.FadeIn); - ReplayFile.Store8((byte)(settings.FadeOut > 0 ? 1 : 0)); - ReplayFile.Store8((byte)(settings.Pushback ? 1 : 0)); - ReplayFile.StoreDouble(settings.CameraParallax); - ReplayFile.StoreDouble(settings.FoV.Value); - ReplayFile.StoreDouble(settings.NoteSize); - ReplayFile.StoreDouble(settings.Sensitivity); - - ReplayAttemptStatusOffset = (uint)ReplayFile.GetPosition(); - - ReplayFile.Store8(0); // reserve attempt status - - string modifiers = ""; - string player = "You"; - - foreach (KeyValuePair mod in Mods) - { - if (mod.Value) - { - modifiers += $"{mod.Key}_"; - } - } - - modifiers = modifiers.TrimSuffix("_"); - - ReplayFile.Store32((uint)modifiers.Length); - ReplayFile.StoreString(modifiers); - ReplayFile.Store32((uint)mapFileName.Length); - ReplayFile.StoreString(mapFileName); - ReplayFile.Store64((ulong)Map.Notes.Length); - ReplayFile.Store32((uint)player.Length); - ReplayFile.StoreString(player); - - ReplayFrameCountOffset = (uint)ReplayFile.GetPosition(); - - ReplayFile.Store64(0); // reserve frame count - } - else if (IsReplay) - { - foreach (Replay replay in Replays) - { - if (replay.Length > LongestReplayLength) - { - LongestReplayLength = replay.Length; - } - } - } - - foreach (KeyValuePair entry in Mods) - { - if (entry.Value) - { - bool hasMultiplier = Constants.MODS_MULTIPLIER_INCREMENT.TryGetValue(entry.Key, out double multiplier); - ModsMultiplier += hasMultiplier ? multiplier : 0; - } - } - } - - public void Hit(int index) - { - Hits++; - Sum++; - Accuracy = Math.Floor((float)Hits / Sum * 10000) / 100; - Combo++; - ComboMultiplierProgress++; - - LastHitColour = SkinManager.Instance.Skin.NoteColors[index % SkinManager.Instance.Skin.NoteColors.Length]; - - float lateness = IsReplay ? HitsInfo[index] : (float)(((int)Progress - Map.Notes[index].Millisecond) / Speed); - float factor = 1 - Math.Max(0, lateness - 25) / 150f; - - if (!IsReplay) - { - Stats.NotesHit++; - - if (Combo > Stats.HighestCombo) - { - Stats.HighestCombo = Combo; - } - - HitsInfo[index] = lateness; - } - - if (ComboMultiplierProgress == ComboMultiplierIncrement) - { - if (ComboMultiplier < 8) - { - ComboMultiplierProgress = ComboMultiplier == 7 ? ComboMultiplierIncrement : 0; - ComboMultiplier++; - - if (ComboMultiplier == 8) - { - multiplierColour = Color.Color8(255, 140, 0); - } - } - } - - uint hitScore = (uint)(100 * ComboMultiplier * ModsMultiplier * factor * ((Speed - 1) / 2.5 + 1)); - - Score += hitScore; - HealthStep = Math.Max(HealthStep / 1.45, 15); - Health = Math.Min(100, Health + HealthStep / 1.75); - Map.Notes[index].Hit = true; - - scoreLabel.Text = Util.String.PadMagnitude(Score.ToString()); - multiplierLabel.Text = $"{ComboMultiplier}x"; - hitsLabel.Text = $"{Hits}"; - hitsLabel.LabelSettings.FontColor = Color.Color8(255, 255, 255, 255); - sumLabel.Text = Util.String.PadMagnitude(Sum.ToString()); - accuracyLabel.Text = $"{(Hits + Misses == 0 ? "100.00" : Accuracy.ToString("F2"))}%"; - comboLabel.Text = Combo.ToString(); - - if (!settings.AlwaysPlayHitSound.Value) - { - SoundManager.HitSound.Play(); - } - - hitTween?.Kill(); - hitTween = hitsLabel.CreateTween(); - hitTween.TweenProperty(hitsLabel.LabelSettings, "font_color", Color.Color8(255, 255, 255, 160), 1); - hitTween.Play(); - - if (!settings.HitPopups || hitPopups >= 64) - { - return; - } - - hitPopups++; - - Label3D popup = hit_feedback.Instantiate(); - node.AddChild(popup); - popup.GlobalPosition = new Vector3(Map.Notes[index].X, -1.4f, 0); - popup.Text = hitScore.ToString(); - - Tween tween = popup.CreateTween(); - tween.TweenProperty(popup, "transparency", 1, 0.25f); - tween.Parallel().TweenProperty(popup, "position", popup.Position + Vector3.Up / 4f, 0.25f).SetTrans(Tween.TransitionType.Quint).SetEase(Tween.EaseType.Out); - tween.TweenCallback(Callable.From(() => - { - hitPopups--; - popup.QueueFree(); - })); - tween.Play(); - } - - public void Miss(int index) - { - Misses++; - Sum++; - Accuracy = Mathf.Floor((float)Hits / Sum * 10000) / 100; - Combo = 0; - ComboMultiplierProgress = 0; - ComboMultiplier = Math.Max(1, ComboMultiplier - 1); - Health = Math.Max(0, Health - HealthStep); - HealthStep = Math.Min(HealthStep * 1.2, 100); - - if (!IsReplay) - { - HitsInfo[index] = -1; - Stats.NotesMissed++; - } - - //if (Health - HealthStep <= 0) - //{ - // Bell.Play(); - // Jesus.Modulate = Color.Color8(255, 255, 255, 196); - // - // JesusTween?.Kill(); - // JesusTween = Jesus.CreateTween(); - // JesusTween.TweenProperty(Jesus, "modulate", Color.Color8(255, 255, 255, 0), 1); - // JesusTween.Play(); - //} - - if (!IsReplay && Health <= 0) - { - if (Alive) - { - Alive = false; - Qualifies = false; - DeathTime = Progress; - SoundManager.FailSound.Play(); - - healthTexture.Modulate = Color.Color8(255, 255, 255, 128); - healthTexture.GetParent().GetNode("Background").Modulate = healthTexture.Modulate; - } - - if (!Mods["NoFail"]) - { - QueueStop(); - } - } - - multiplierLabel.Text = $"{ComboMultiplier}x"; - missesLabel.Text = $"{Misses}"; - simpleMissesLabel.Text = $"{Misses}"; - missesLabel.LabelSettings.FontColor = Color.Color8(255, 255, 255, 255); - sumLabel.Text = Util.String.PadMagnitude(Sum.ToString()); - accuracyLabel.Text = $"{(Hits + Misses == 0 ? "100.00" : Accuracy.ToString("F2"))}%"; - comboLabel.Text = Combo.ToString(); - - missTween?.Kill(); - missTween = missesLabel.CreateTween(); - missTween.TweenProperty(missesLabel.LabelSettings, "font_color", Color.Color8(255, 255, 255, 160), 1); - missTween.Play(); - - if (!settings.MissPopups || missPopups >= 64) - { - return; - } - - missPopups++; - - Sprite3D icon = miss_feedback.Instantiate(); - node.AddChild(icon); - icon.GlobalPosition = new Vector3(Map.Notes[index].X, -1.4f, 0); - icon.Texture = SkinManager.Instance.Skin.MissFeedbackImage; - - Tween tween = icon.CreateTween(); - tween.TweenProperty(icon, "transparency", 1, 0.25f); - tween.Parallel().TweenProperty(icon, "position", icon.Position + Vector3.Up / 4f, 0.25f).SetTrans(Tween.TransitionType.Quint).SetEase(Tween.EaseType.Out); - tween.TweenCallback(Callable.From(() => - { - missPopups--; - icon.QueueFree(); - })); - tween.Play(); - } - - public void Stop() - { - if (Stopped) - { - return; - } - - Stopped = true; - - if (!IsReplay && ReplayFile != null) - { - ReplayFile.Seek(ReplayAttemptStatusOffset); - ReplayFile.Store8((byte)(Alive ? (Qualifies ? 0 : 1) : 2)); - - ReplayFile.Seek(ReplayFrameCountOffset); - ReplayFile.Store64((ulong)ReplayFrames.Count); - - foreach (float[] frame in ReplayFrames) - { - ReplayFile.StoreFloat(frame[0]); - ReplayFile.StoreFloat(frame[1]); - ReplayFile.StoreFloat(frame[2]); - } - - ReplayFile.Seek(ReplayFile.GetLength()); - ReplayFile.Store64(FirstNote); - ReplayFile.Store64(Sum); - - for (ulong i = FirstNote; i < FirstNote + Sum; i++) - { - ReplayFile.Store8((byte)(HitsInfo[i] == -1 ? 255 : Math.Min(254, HitsInfo[i] * (254 / 55)))); - } - - ReplayFile.Store64((ulong)ReplaySkips.Count); - - foreach (float skip in ReplaySkips) - { - ReplayFile.StoreFloat(skip); - } - - ReplayFile.Close(); - ReplayFile = Godot.FileAccess.Open($"{Constants.USER_FOLDER}/replays/{ID}.phxr", Godot.FileAccess.ModeFlags.ReadWrite); - - ulong length = ReplayFile.GetLength(); - byte[] hash = SHA256.HashData(ReplayFile.GetBuffer((long)length)); - - ReplayFile.StoreBuffer(hash); - ReplayFile.Close(); - - CurrentAttempt.HitsInfo = CurrentAttempt.HitsInfo[0..(int)PassedNotes]; - } - else if (IsReplay) - { - CurrentAttempt.HitsInfo = CurrentAttempt.HitsInfo[0..(int)CurrentAttempt.Replays[0].LastNote]; - } - } - } - - public override void _Ready() - { - base._Ready(); - - settings = SettingsManager.Instance.Settings; - - node = this; - - menu = GetNode("Menu"); - //fpsCounter = GetNode