diff --git a/prefabs/map_info_container.tscn b/prefabs/map_info_container.tscn index 98ccc0ce..7dc3c47d 100644 --- a/prefabs/map_info_container.tscn +++ b/prefabs/map_info_container.tscn @@ -6,6 +6,7 @@ [ext_resource type="Theme" uid="uid://dhuhu3mmaew1a" path="res://themes/theme.tres" id="4_4d08m"] [ext_resource type="Script" uid="uid://c1235cyjfh4rl" path="res://scripts/ui/LinkPopupButton.cs" id="4_dccrs"] [ext_resource type="Texture2D" uid="uid://7px5v8ikildx" path="res://user/skins/default/ui/buttons/favorite.png" id="5_1m5pv"] +[ext_resource type="Texture2D" uid="uid://dkpbociv8fgfd" path="res://user/skins/default/ui/buttons/export.png" id="5_g02bu"] [ext_resource type="Script" uid="uid://dy45uerqy0yd7" path="res://scripts/ui/FlatPreview.cs" id="5_svokg"] [ext_resource type="Texture2D" uid="uid://degvlg4p0if70" path="res://user/skins/default/ui/buttons/add_video.png" id="6_g02bu"] [ext_resource type="Texture2D" uid="uid://cgxbwx4f28lt2" path="res://user/skins/default/ui/buttons/copy.png" id="7_oibm8"] @@ -175,7 +176,7 @@ outline_color = Color(0, 0, 0, 1) shadow_size = 4 shadow_color = Color(0, 0, 0, 1) -[node name="MapInfoContainer" type="Panel" unique_id=150923669 node_paths=PackedStringArray("info", "dim", "coverBackground", "cover", "infoSubholder", "mainLabel", "extraLabel", "artistLink", "favoriteButton", "videoButton", "videoDialog", "copyButton", "copyDialog", "deleteButton", "actions", "preview", "speedHolder", "speedPresets", "playHolder", "startButton", "leaderboard", "lbScrollContainer", "lbContainer", "lbExpand", "lbHide")] +[node name="MapInfoContainer" type="Panel" unique_id=150923669 node_paths=PackedStringArray("info", "dim", "coverBackground", "cover", "infoSubholder", "mainLabel", "extraLabel", "artistLink", "exportButton", "favoriteButton", "videoButton", "videoDialog", "copyButton", "copyDialog", "deleteButton", "actions", "preview", "speedHolder", "speedPresets", "playHolder", "startButton", "leaderboard", "lbScrollContainer", "lbContainer", "lbExpand", "lbHide")] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -193,6 +194,7 @@ infoSubholder = NodePath("Info/ScrollContainer/HBoxContainer") mainLabel = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Main") extraLabel = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Extra") artistLink = NodePath("Info/ScrollContainer/HBoxContainer/Background/ArtistLink") +exportButton = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Buttons/Export") favoriteButton = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Buttons/Favorite") videoButton = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Buttons/Video") videoDialog = NodePath("Info/ScrollContainer/HBoxContainer/Subholder/Buttons/Video/VideoDialog") @@ -357,6 +359,17 @@ grow_horizontal = 0 grow_vertical = 0 alignment = 2 +[node name="Export" type="Button" parent="Info/ScrollContainer/HBoxContainer/Subholder/Buttons" unique_id=2033571562] +layout_mode = 2 +tooltip_text = "Export (Shift+Click for .sspm export)" +focus_mode = 0 +mouse_default_cursor_shape = 2 +theme = ExtResource("4_4d08m") +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_constants/icon_max_width = 24 +icon = ExtResource("5_g02bu") +icon_alignment = 1 + [node name="Favorite" type="Button" parent="Info/ScrollContainer/HBoxContainer/Subholder/Buttons" unique_id=1022164792] layout_mode = 2 tooltip_text = "Favorite" @@ -390,6 +403,7 @@ filters = PackedStringArray("*.mp4") use_native_dialog = true [node name="Copy" type="Button" parent="Info/ScrollContainer/HBoxContainer/Subholder/Buttons" unique_id=2053076461] +visible = false layout_mode = 2 tooltip_text = "Copy" focus_mode = 0 diff --git a/scripts/SoundManager.cs b/scripts/SoundManager.cs index 9eb59133..f0ecf4eb 100644 --- a/scripts/SoundManager.cs +++ b/scripts/SoundManager.cs @@ -221,7 +221,7 @@ public static void PlayJukebox(Map map, bool setRichPresence = true) JukeboxIndex = MapManager.Maps.FindIndex(x => x.Id == map.Id); - Song.Stream = Util.Audio.LoadFromFile($"{MapUtil.MapsCacheFolder}/{map.Name}/audio.{map.AudioExt}"); + Song.Stream = Util.Audio.LoadFromFile($"{MapUtil.MapsFolder}/{map.Name}/audio.{map.AudioExt}"); Song.Play(); Instance.JukeboxPlayed?.Invoke(map); @@ -280,7 +280,7 @@ public static void StartMapSelectionPlayback(Map map, bool setRichPresence = tru } } - Song.Stream = Util.Audio.LoadFromFile($"{MapUtil.MapsCacheFolder}/{map.Name}/audio.{map.AudioExt}"); + Song.Stream = Util.Audio.LoadFromFile($"{MapUtil.MapsFolder}/{map.Name}/audio.{map.AudioExt}"); Song.Play(0); Instance.JukeboxPlayed?.Invoke(map); diff --git a/scripts/game/managers/ReplayManager.cs b/scripts/game/managers/ReplayManager.cs index 42166b59..ae3d0269 100644 --- a/scripts/game/managers/ReplayManager.cs +++ b/scripts/game/managers/ReplayManager.cs @@ -1,4 +1,7 @@ using System; +using System.IO; + +// using System.IO; using System.Linq; using System.Security.Cryptography; using Godot; @@ -28,7 +31,7 @@ public enum Mode public string ReplayPath; public Vector2 CursorPosition { get; private set; } - private FileAccess file; + private Godot.FileAccess file; private ulong statusOffset, frameCountOffset; public void NewReplay(Attempt attempt) @@ -39,7 +42,7 @@ public void NewReplay(Attempt attempt) ReplayPath = $"{Constants.USER_FOLDER}/replays/{attempt.ID}.phxr"; - file = FileAccess.Open(ReplayPath, FileAccess.ModeFlags.Write); + file = Godot.FileAccess.Open(ReplayPath, Godot.FileAccess.ModeFlags.Write); file.StoreString("phxr"); // sig file.Store8(1); // replay file version @@ -69,7 +72,8 @@ public void NewReplay(Attempt attempt) } string serializedMods = string.Join("_", mods); - string mapName = attempt.Map.FilePath.GetFile().GetBaseName(); + // string mapName = attempt.Map.FilePath.GetFile().GetBaseName(); + string mapName = Path.GetFileName(attempt.Map.FolderPath); string player = "You"; void storeSizedString(string data) @@ -126,7 +130,7 @@ public void SaveReplay(Attempt attempt) file.Close(); // open replay to store hash - file = FileAccess.Open($"{Constants.USER_FOLDER}/replays/{attempt.ID}.phxr", FileAccess.ModeFlags.ReadWrite); + file = Godot.FileAccess.Open($"{Constants.USER_FOLDER}/replays/{attempt.ID}.phxr", Godot.FileAccess.ModeFlags.ReadWrite); ulong length = file.GetLength(); byte[] hash = SHA256.HashData(file.GetBuffer((long)length)); file.StoreBuffer(hash); diff --git a/scripts/game/ui/PlaytestOverlay.cs b/scripts/game/ui/PlaytestOverlay.cs index b6917758..1b6102dc 100644 --- a/scripts/game/ui/PlaytestOverlay.cs +++ b/scripts/game/ui/PlaytestOverlay.cs @@ -74,7 +74,7 @@ public void UpdatePlaytestOverlay(bool show) speedValue = speedDouble; } - var map = MapParser.Decode(oldAttempt.Map.FilePath, Rhythia.AudioFilePath); + var map = MapParser.Decode(oldAttempt.Map.FolderPath, Rhythia.AudioFilePath); Game.Attempt = new(map, speedValue, GetStartFrom(startFromEdit) * 1000, Rhythia.TempCam, Rhythia.TempMods); SceneManager.ReloadCurrentScene(); diff --git a/scripts/map/Map.cs b/scripts/map/Map.cs index 15c8c76f..afbb3638 100644 --- a/scripts/map/Map.cs +++ b/scripts/map/Map.cs @@ -17,15 +17,20 @@ public partial class Map : RefCounted public int Id { get; set; } public string Name { get; set; } = string.Empty; + public string LastModifiedMetadata { get; set; } + public string LastModifiedNotes { get; set; } - public string Hash { get; set; } + /// + /// The hash of the metadata.json, then the objects.phxmo in order + /// + public string MetadataObjectHash { get; set; } public string Collection { get; set; } = string.Empty; [Ignore] public MapSet MapSet { get; set; } - public string FilePath { get; set; } = string.Empty; + public string FolderPath { get; set; } = string.Empty; public bool Favorite { get; set; } @@ -86,7 +91,7 @@ public Note[] TryParseNotes() { try { - notes = MapParser.DecodePHXMO($"{MapUtil.MapsCacheFolder}/{Name}/objects.phxmo"); + notes = MapParser.DecodePHXMO($"{MapUtil.MapsFolder}/{Name}/objects.phxmo"); return notes; } catch @@ -97,7 +102,7 @@ public Note[] TryParseNotes() private Texture2D getCover() { - string path = $"{MapUtil.MapsCacheFolder}/{Name}"; + string path = $"{MapUtil.MapsFolder}/{Name}"; if (cover == DefaultCover && File.Exists($"{path}/cover.png")) { @@ -115,10 +120,13 @@ private Texture2D getCover() public Map() { } - public Map(string filePath, Note[] data = null, string id = null, string artist = "", string title = "", float rating = 0, string[] mappers = null, int difficulty = 0, string difficultyName = null, int? length = null, byte[] audioBuffer = null, byte[] coverBuffer = null, byte[] videoBuffer = null, bool ephemeral = false, string artistLink = "", string artistPlatform = "") + public Map(string folderPath, string metadataObjHash = "", string lastModifiedMetadata = "", string lastModifiedNotes = "", Note[] data = null, string id = null, string artist = "", string title = "", float rating = 0, string[] mappers = null, int difficulty = 0, string difficultyName = null, int? length = null, byte[] audioBuffer = null, byte[] coverBuffer = null, byte[] videoBuffer = null, bool ephemeral = false, string artistLink = "", string artistPlatform = "") { - FilePath = filePath; + FolderPath = folderPath; Ephemeral = ephemeral; + MetadataObjectHash = metadataObjHash; + LastModifiedMetadata = lastModifiedMetadata; + LastModifiedNotes = lastModifiedNotes; Artist = (artist ?? "").StripEscapes(); ArtistLink = artistLink; ArtistPlatform = artistPlatform; diff --git a/scripts/map/MapCache.cs b/scripts/map/MapCache.cs index ac1d6929..cba136b7 100644 --- a/scripts/map/MapCache.cs +++ b/scripts/map/MapCache.cs @@ -1,11 +1,14 @@ -using System; +using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.IO; using System.IO.Compression; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using Godot; +using Octokit; using Util; public static class MapCache @@ -28,12 +31,28 @@ public static void Load(bool fullSync) try { - string[] files = Directory.GetFiles(MapUtil.MapsFolder, $"*.{Constants.DEFAULT_MAP_EXT}", SearchOption.AllDirectories); + // string[] files = Directory.GetFiles(MapUtil.MapsFolder, $"*.{Constants.DEFAULT_MAP_EXT}", SearchOption.AllDirectories); + + // List mapsList = Directory + // .GetFiles(MapUtil.MapsFolder, $"*.{Constants.DEFAULT_MAP_EXT}", SearchOption.AllDirectories) + // .Concat(Directory.GetDirectories(MapUtil.MapsFolder, "*", SearchOption.AllDirectories)) + // .ToList(); + + // Map files go first since they will be encoded to folders after they get parsed in MapParser.cs + List mapsList = Directory.GetFiles(MapUtil.MapsFolder,$"*.{Constants.DEFAULT_MAP_EXT}", SearchOption.AllDirectories) + .Concat(Directory.GetDirectories(MapUtil.MapsFolder, "*", SearchOption.AllDirectories)) + .ToList(); + + // List mapsList = Directory.GetDirectories(MapUtil.MapsFolder, "*", SearchOption.AllDirectories) + // .Concat(Directory.GetFiles(MapUtil.MapsFolder,$"*.{Constants.DEFAULT_MAP_EXT}", SearchOption.AllDirectories)) + // .ToList(); + + string[] toParseMaps = mapsList.ToArray(); if (fullSync) { - syncFiles(files); - addNonCachedFiles(files); + syncFiles(toParseMaps); + addNonCachedFiles(toParseMaps); OnFilesSyncFinished?.Invoke(FilesSynced.Value); FilesToSync.Value = 0; @@ -48,36 +67,52 @@ public static void Load(bool fullSync) } } - private static void syncFiles(string[] files) + private static void syncFiles(string[] toParseMaps) { var maps = FetchAll(); FilesToSync.Value = maps.Count; FilesSynced.Value = 0; - for (int i = 0; i < files.Length; i++) + for (int i = 0; i < toParseMaps.Length; i++) { - files[i] = BackSlashToForwardSlash(files[i]); + toParseMaps[i] = BackSlashToForwardSlash(toParseMaps[i]); } - var filesHashSet = files.ToHashSet(); + var mapsHashSet = toParseMaps.ToHashSet(); foreach (var map in maps) { - string filePath = BackSlashToForwardSlash(map.FilePath); - if (filesHashSet.Contains(filePath)) + string mapPath = BackSlashToForwardSlash(map.FolderPath); + + // Checks if the map is actually inside the maps folder + if (mapsHashSet.Contains(mapPath)) { - string checksum = GetMd5Checksum(filePath); + DateTime metadataModifiedDate = File.GetLastWriteTime(Path.Combine(mapPath, "metadata.json")); + DateTime objectModifiedDate = File.GetLastWriteTime(Path.Combine(mapPath, "objects.phxmo")); + + // i have to convert to string since sqlite doesnt support DateTime c# conversion + // PLUS, theres like seconds or something and it doesnt catch that soooooo + string metadataResult = metadataModifiedDate.ToString(); + string notesResult = objectModifiedDate.ToString(); + + bool metadataCheck = map.LastModifiedMetadata == metadataResult; + bool objectsCheck = map.LastModifiedNotes == notesResult; - if (map.Hash == checksum) + // last modified is faster and lowkey more important -fog + if (metadataCheck || objectsCheck) { - if (!Directory.Exists($"{MapUtil.MapsCacheFolder}/{map.Name}")) - { - InsertIntoMapCacheFolder(map); - } + FilesSynced.Value++; + continue; + } - FilesSynced.Value += 1; + // we will do checksum checking after if last modified dates dont match + // this code shouldn't be reached unless the dates dont match + string checksum = GetMd5Checksum(mapPath); + if (map.MetadataObjectHash == checksum) + { + FilesSynced.Value++; continue; } @@ -85,103 +120,93 @@ private static void syncFiles(string[] files) try { - newMap = MapParser.Decode(filePath, null, false, true); + newMap = MapParser.Decode(mapPath, null, false, true); } catch (Exception ex) { Logger.Error(ex); - File.Delete(filePath); + if (File.Exists(mapPath)) + { + File.Delete(mapPath); + } + else if (Directory.Exists(mapPath)) + { + Directory.Delete(mapPath, true); + } DatabaseService.Connection.Delete(map); - FilesSynced.Value += 1; + FilesSynced.Value++; continue; } + newMap.LastModifiedMetadata = metadataModifiedDate.ToString(); + newMap.LastModifiedNotes = objectModifiedDate.ToString(); + newMap.Id = map.Id; - newMap.Hash = checksum; + newMap.MetadataObjectHash = checksum; DatabaseService.Connection.Update(newMap); - InsertIntoMapCacheFolder(map); + // InsertIntoMapCacheFolder(map); Logger.Log($"Updated cached map: {newMap.Name}"); - - FilesSynced.Value += 1; + FilesSynced.Value++; continue; } else { - removeCacheFolder(map); + if (Directory.Exists($"{MapUtil.MapsFolder}/{map.Name}")) + { + Directory.Delete($"{MapUtil.MapsFolder}/{map.Name}", true); + } + DatabaseService.Connection.Delete(map); - Logger.Log($"Removed {filePath} from the cache, as it no longer exists."); + Logger.Log($"Removed {mapPath} from the cache, as it no longer exists."); - FilesSynced.Value += 1; + FilesSynced.Value++; } } } - public static void InsertIntoMapCacheFolder(Map map) - { - string path = $"{MapUtil.MapsCacheFolder}/{map.Name}"; - using var stream = File.OpenRead(map.FilePath); - var archive = new ZipArchive(stream); - - if (Directory.Exists(path)) - { - Directory.Delete(path, true); - } - - archive.ExtractToDirectory(path, true); - } - - private static void removeCacheFolder(Map map) - { - try - { - Directory.Delete($"{MapUtil.MapsCacheFolder}/{map.Name}", true); - } - catch - { - return; - } - } - - private static void addNonCachedFiles(string[] files) + private static void addNonCachedFiles(string[] toParseMaps) { var maps = FetchAll(); HashSet hashSet = new(); - maps.ForEach(map => hashSet.Add(map.FilePath)); + maps.ForEach(map => hashSet.Add(map.FolderPath)); - foreach (string file in files) + // Maps that need to be parsed - maps in database cache + FilesToSync.Value = toParseMaps.Count() - maps.Count(); + FilesSynced.Value = 0; + + foreach (string toParseMap in toParseMaps) { - if (hashSet.Contains(BackSlashToForwardSlash(file))) + if (hashSet.Contains(BackSlashToForwardSlash(toParseMap))) { continue; } - FilesToSync.Value += 1; + // FilesToSync.Value += 1; try { - var map = MapParser.Decode(file); - map.Collection = file.GetBaseDir().Split("/")[^1]; - map.FilePath = $"{Constants.USER_FOLDER}/maps/{map.Collection}/{map.Name}.{Constants.DEFAULT_MAP_EXT}"; - map.Hash = GetMd5Checksum(file); - File.Move(file, map.FilePath); + GD.Print($"decoding: {toParseMap}\nfilestosync: {FilesToSync.Value}, filessynced: {FilesSynced.Value}"); + var map = MapParser.Decode(toParseMap); + map.FolderPath = $"{Constants.USER_FOLDER}/maps/{map.Name}"; + map.MetadataObjectHash = GetMd5Checksum(map.FolderPath); InsertMap(map); } catch { - File.Delete(file); + Directory.Delete(toParseMap, true); Logger.Log($"Failed to add map non-cached map"); } - FilesSynced.Value += 1; + FilesSynced.Value++; } } public static int InsertMap(Map map) { - var existing = DatabaseService.Connection.Find(x => x.Hash == x.Hash); + var existing = DatabaseService.Connection.Find(x => x.MetadataObjectHash == map.MetadataObjectHash); var updated = DatabaseService.Connection.Find(x => x.Name == map.Name); try @@ -194,9 +219,8 @@ public static int InsertMap(Map map) } DatabaseService.Connection.Insert(map); - InsertIntoMapCacheFolder(map); - return DatabaseService.Connection.Get(x => x.Hash == map.Hash).Id; + return DatabaseService.Connection.Get(x => x.MetadataObjectHash == map.MetadataObjectHash).Id; } catch (Exception e) { @@ -206,12 +230,12 @@ public static int InsertMap(Map map) return -1; } - string newPath = Path.Combine(MapUtil.MapsFolder, map.Collection, map.Name); - string existingPath = Path.Combine(MapUtil.MapsFolder, map.Collection, existing?.FilePath ?? updated.FilePath); + string newPath = Path.Combine(MapUtil.MapsFolder, map.Name); + string existingPath = Path.Combine(MapUtil.MapsFolder, existing?.FolderPath ?? updated.FolderPath); if (existingPath != newPath) { - File.Delete(newPath); + Directory.Delete(newPath, true); return -1; } @@ -224,7 +248,6 @@ public static void UpdateMap(Map map) try { DatabaseService.Connection.Update(map); - InsertIntoMapCacheFolder(map); } catch (Exception e) { @@ -253,7 +276,7 @@ public static void OrderAndSetMaps() { foreach (var map in maps) { - string path = $"{MapUtil.MapsCacheFolder}/{map.Name}"; + string path = $"{MapUtil.MapsFolder}/{map.Name}"; if (map.Cover == Map.DefaultCover && File.Exists($"{path}/cover.png")) { @@ -275,7 +298,6 @@ public static void OrderAndSetMaps() } }).CallDeferred(); } - } } }); @@ -298,39 +320,37 @@ public static void OrderAndSetMaps() MapManager.Maps = sortedMaps; } - public static List ConvertToMapSets(IEnumerable maps) - { - var groupedMaps = maps - .GroupBy(u => u.Collection) - .Select(x => x.ToList()) - .ToList(); + // public static List ConvertToMapSets(IEnumerable maps) + // { + // var groupedMaps = maps + // .GroupBy(u => u.Collection) + // .Select(x => x.ToList()) + // .ToList(); - var mapSets = new List(); + // var mapSets = new List(); - foreach (var mapSet in groupedMaps) - { - var set = new MapSet() - { - Directory = mapSet.First().Collection, - Maps = mapSet - }; + // foreach (var mapSet in groupedMaps) + // { + // var set = new MapSet() + // { + // Directory = mapSet.First().Collection, + // Maps = mapSet + // }; - set.Maps.ForEach(x => x.MapSet = set); - mapSets.Add(set); - } + // set.Maps.ForEach(x => x.MapSet = set); + // mapSets.Add(set); + // } - return mapSets; - } + // return mapSets; + // } public static string GetMd5Checksum(string path) { - using (var md5 = MD5.Create()) - { - using (var stream = File.OpenRead(path)) - { - return BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", string.Empty).ToLower(); - } - } + string metadataPath = Path.Combine(path, "metadata.json"); + string objectsPath = Path.Combine(path, "objects.phxmo"); + byte[] hash = Misc.HashFiles([metadataPath, objectsPath]); + + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLower(); } public static List FetchAll() => DatabaseService.Connection.Table().ToList(); diff --git a/scripts/map/MapManager.cs b/scripts/map/MapManager.cs index df690414..7f09461c 100644 --- a/scripts/map/MapManager.cs +++ b/scripts/map/MapManager.cs @@ -41,7 +41,8 @@ public static void Select(Map map) public static Map GetMapById(int id) { - return Maps.Where(x => x.Id == id).First(); + // return Maps.Where(x => x.Id == id).First(); + return Maps.FirstOrDefault(x => x.Id == id); } public static void Update(Map map) @@ -58,35 +59,35 @@ public static void InsertVideo(Map map, string path) map.VideoBuffer = videoBuffer; - var oldmap = MapParser.Decode(map.FilePath); + var oldmap = MapParser.Decode(map.FolderPath); map.Mappers = map.CachedMappers.Split("_"); map.AudioBuffer = oldmap.AudioBuffer; map.CoverBuffer = oldmap.CoverBuffer; - File.Delete(map.FilePath); + Directory.Delete(map.FolderPath); MapParser.Encode(map); - map.Hash = MapCache.GetMd5Checksum(map.FilePath); + map.FolderPath = MapCache.GetMd5Checksum(map.FolderPath); Update(map); } public static void RemoveVideo(Map map) { - _ = Godot.FileAccess.Open(map.FilePath, Godot.FileAccess.ModeFlags.Read); + _ = Godot.FileAccess.Open(map.FolderPath, Godot.FileAccess.ModeFlags.Read); map.VideoBuffer = null; - var oldmap = MapParser.Decode(map.FilePath); + var oldmap = MapParser.Decode(map.FolderPath); map.Mappers = map.PrettyMappers.Split(" "); map.AudioBuffer = oldmap.AudioBuffer; map.CoverBuffer = oldmap.CoverBuffer; - File.Delete(map.FilePath); + File.Delete(map.FolderPath); MapParser.Encode(map); - map.Hash = MapCache.GetMd5Checksum(map.FilePath); + map.MetadataObjectHash = MapCache.GetMd5Checksum(map.FolderPath); Update(map); } @@ -96,11 +97,11 @@ public static void Delete(Map map) { try { - File.Delete(map.FilePath); + File.Delete(map.FolderPath); } catch { - if (File.Exists(map.FilePath)) + if (File.Exists(map.FolderPath)) { Logger.Error("Unable to delete map"); return; diff --git a/scripts/map/MapParser.cs b/scripts/map/MapParser.cs index c1fc5f92..06854e67 100644 --- a/scripts/map/MapParser.cs +++ b/scripts/map/MapParser.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Godot; using Godot.Collections; +using Util; public partial class MapParser : Node { @@ -27,7 +28,7 @@ public static async Task BulkImport(string[] files, bool notify = false) { if (files.Length == 0 || files == null) return; - if (notify) ToastNotification.Notify($"Importing {files.Length} map(s)"); + if (notify) _ = ToastNotification.Notify($"Importing {files.Length} map(s)"); await Task.Run(() => { @@ -64,26 +65,15 @@ await Task.Run(() => }); SoundManager.UpdateJukeboxQueue(); - if (notify) ToastNotification.Notify($"Finished importing {files.Length} map(s)"); + if (notify) _ = ToastNotification.Notify($"Finished importing {files.Length} map(s)"); } - public static void Encode(Map map, bool logBenchmark = false) + public static void ExportEncode(Map map) { - double start = Time.GetTicksUsec(); - - map.Collection = $"default"; + string exportPath = $"{Constants.USER_FOLDER}/export/"; + string exportFilePath = Path.Combine(exportPath, $"{map.Name}.phxm"); - string mapDirectory = $"{Constants.USER_FOLDER}/maps/{map.Collection}"; - string mapFilePath = Path.Combine(mapDirectory, $"{map.Name}.{Constants.DEFAULT_MAP_EXT}"); - - if (!Directory.Exists(mapDirectory)) Directory.CreateDirectory(mapDirectory); - - /* - uint32; ms - 1 byte; quantum - 1 byte OR int32; x - 1 byte OR int32; y - */ + if (!Directory.Exists(exportPath)) Directory.CreateDirectory(exportPath); using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create)) @@ -138,9 +128,92 @@ void addAsset(string name, byte[] buffer) if (map.VideoBuffer != null) addAsset($"video.mp4", map.VideoBuffer); } - map.Hash = Convert.ToHexString(MD5.HashData(ms.ToArray())).ToLower(); - File.WriteAllBytes(mapFilePath, ms.ToArray()); - map.FilePath = mapFilePath; + File.WriteAllBytes(exportFilePath, ms.ToArray()); + } + + public static void Encode(Map map, bool logBenchmark = false) + { + double start = Time.GetTicksUsec(); + + string mapDirectory = $"{Constants.USER_FOLDER}/maps"; + string mapFolderPath = Path.Combine(mapDirectory, $"{map.Name}"); + // string mapFilePath = Path.Combine(mapDirectory, $"{map.Name}.{Constants.DEFAULT_MAP_EXT}"); + + if (!Directory.Exists(mapDirectory)) Directory.CreateDirectory(mapDirectory); + if (!Directory.Exists(mapFolderPath)) Directory.CreateDirectory(mapFolderPath); + + /* + uint32; ms + 1 byte; quantum + 1 byte OR int32; x + 1 byte OR int32; y + */ + + File.WriteAllText(Path.Combine(mapFolderPath, "metadata.json"), map.EncodeMeta()); + + using var stream = File.Create(Path.Combine(mapFolderPath, "objects.phxmo")); + // using BinaryWriter bw = new BinaryWriter(stream); + using (var bw = new BinaryWriter(stream)) + { + bw.Write((uint)12); + bw.Write((uint)map.Notes.Length); + foreach (var note in map.Notes) + { + bool quantum = (int)note.X != note.X || (int)note.Y != note.Y || note.X < -1 || note.X > 1 || note.Y < -1 || note.Y > 1; + bw.Write((uint)note.Millisecond); + bw.Write(Convert.ToByte(quantum)); + if (quantum) + { + bw.Write((float)note.X); + bw.Write((float)note.Y); + } + else + { + bw.Write((byte)(note.X + 1)); + bw.Write((byte)(note.Y + 1)); + } + } + + bw.Write(0); // timing point count + bw.Write(0); // brightness count + bw.Write(0); // contrast count + bw.Write(0); // saturation count + bw.Write(0); // blur count + bw.Write(0); // fov count + bw.Write(0); // tint count + bw.Write(0); // position count + bw.Write(0); // rotation count + bw.Write(0); // ar factor count + bw.Write(0); // text count + } + + void addAsset(string name, byte[] buffer) + { + // var asset = archive.CreateEntry(name, CompressionLevel.NoCompression); + // using var stream = asset.Open(); + // stream.Write(buffer, 0, buffer.Length); + + string assetPath = Path.Combine(mapFolderPath, name); + File.WriteAllBytes(assetPath, buffer); + } + + if (map.AudioBuffer != null) addAsset($"audio.{map.AudioExt}", map.AudioBuffer); + if (map.CoverBuffer != null) addAsset($"cover.png", map.CoverBuffer); + if (map.VideoBuffer != null) addAsset($"video.mp4", map.VideoBuffer); + + byte[] hash = Misc.HashFiles([Path.Combine(mapFolderPath, "metadata.json"), Path.Combine(mapFolderPath, "objects.phxmo")]); + + map.MetadataObjectHash = BitConverter.ToString(hash).Replace("-", "").ToLower(); + + + DateTime metadataModified = File.GetLastWriteTime(Path.Combine(mapFolderPath, "metadata.json")); + DateTime objectsModified = File.GetLastWriteTime(Path.Combine(mapFolderPath, "objects.phxmo")); + map.LastModifiedMetadata = metadataModified.ToString(); + map.LastModifiedNotes = objectsModified.ToString(); + + map.FolderPath = mapFolderPath; + // need to do map caching stuff here + MapCache.InsertMap(map); if (logBenchmark) @@ -151,34 +224,47 @@ void addAsset(string name, byte[] buffer) public static Map Decode(string path, string audio = null, bool logBenchmark = false, bool save = false) { - if (!File.Exists(path)) + // if (!File.Exists(path)) + // { + // ToastNotification.Notify($"Invalid file path", 2); + // throw Logger.Error($"Invalid file path ({path})"); + // } + + // Extract any .phxm files or encode other formats if needed + if (File.Exists(path)) { - ToastNotification.Notify($"Invalid file path", 2); - throw Logger.Error($"Invalid file path ({path})"); - } + string ext = path.GetExtension(); + double start = Time.GetTicksUsec(); - string ext = path.GetExtension(); - double start = Time.GetTicksUsec(); + if (!IsValidExt(ext)) + { + _ = ToastNotification.Notify("Unsupported file format", 1); + throw Logger.Error($"Unsupported file format ({ext})"); + } + + Map map = ext switch + { + "phxm" => PHXM(path), + "sspm" => SSPM(path), + "txt" => SSMapV1(path, audio), + "rhm" => RHM(path), + _ => new() + }; - if (!IsValidExt(ext)) + if (logBenchmark) Logger.Log($"DECODING {ext.ToUpper()}: {(Time.GetTicksUsec() - start) / 1000}ms"); + if (save) Encode(map); + + return map; + } + else if (Directory.Exists(path)) { - ToastNotification.Notify("Unsupported file format", 1); - throw Logger.Error($"Unsupported file format ({ext})"); + return PHXMFolder(path); } - - Map map = ext switch + else { - "phxm" => PHXM(path), - "sspm" => SSPM(path), - "txt" => SSMapV1(path, audio), - "rhm" => RHM(path), - _ => new() - }; - - if (logBenchmark) Logger.Log($"DECODING {ext.ToUpper()}: {(Time.GetTicksUsec() - start) / 1000}ms"); - if (save) Encode(map); - - return map; + _ = ToastNotification.Notify($"Invalid map path", 2); + throw Logger.Error($"Invalid map path ({path})"); + } } public static Map SSMapV1(string path, string audioPath = null) { @@ -208,7 +294,7 @@ public static Map SSMapV1(string path, string audioPath = null) audio.Close(); } - map = new(path, notes, null, "", name, audioBuffer: audioBuffer); + map = new(path, "", "", "", notes, null, "", name, audioBuffer: audioBuffer); } catch (Exception exception) { @@ -252,7 +338,7 @@ public static Map SSPM(string path) } catch (Exception exception) { - ToastNotification.Notify($"SSPM file corrupted", 2); + _ = ToastNotification.Notify($"SSPM file corrupted", 2); Logger.Error(exception); throw; } @@ -376,7 +462,7 @@ private static Map sspmV1(FileParser file, string path = null) notes[i].Index = i; } - map = new(path ?? $"{Constants.USER_FOLDER}/maps/{song}_temp.sspm", notes, id, artist, song, 0, mappers, difficulty, null, (int)mapLength, audioBuffer, coverBuffer); + map = new(path ?? $"{Constants.USER_FOLDER}/maps/{song}_temp.sspm", "", "", "", notes, id, artist, song, 0, mappers, difficulty, null, (int)mapLength, audioBuffer, coverBuffer); } catch (Exception exception) { @@ -530,7 +616,7 @@ private static Map sspmV2(FileParser file, string path = null) notes[i].Index = i; } - map = new(path, notes, id, artist, song, 0, mappers, difficulty, difficultyName, (int)mapLength, audioBuffer, coverBuffer); + map = new(path, "", "", "", notes, id, artist, song, 0, mappers, difficulty, difficultyName, (int)mapLength, audioBuffer, coverBuffer); } catch (Exception exception) { @@ -542,48 +628,58 @@ private static Map sspmV2(FileParser file, string path = null) return map; } - public static Map PHXM(string path) + public static Map PHXMFolder(string path) { + Map map; try { - var file = ZipFile.OpenRead(path); + string metadataString = File.ReadAllText($"{path}/metadata.json"); + var metadata = (Dictionary)Json.ParseString(metadataString); + + byte[] objectsBuffer = File.ReadAllBytes($"{path}/objects.phxmo"); - byte[] metaBuffer = getZipEntryBuffer(file, "metadata.json"); - byte[] objectsBuffer = getZipEntryBuffer(file, "objects.phxmo"); byte[] audioBuffer = null; byte[] coverBuffer = null; byte[] videoBuffer = null; - var metadata = (Dictionary)Json.ParseString(Encoding.UTF8.GetString(metaBuffer)); - FileParser objects = new(objectsBuffer); + // FileParser objects = new(objectsBuffer); if ((bool)metadata["HasAudio"]) { - audioBuffer = getZipEntryBuffer(file, $"audio.{metadata["AudioExt"]}"); + audioBuffer = File.ReadAllBytes($"{path}/audio.{metadata["AudioExt"]}"); } if ((bool)metadata["HasCover"]) { - coverBuffer = getZipEntryBuffer(file, "cover.png"); + coverBuffer = File.ReadAllBytes($"{path}/cover.png"); } if ((bool)metadata["HasVideo"]) { - videoBuffer = getZipEntryBuffer(file, "video.mp4"); + videoBuffer = File.ReadAllBytes($"{path}/video.mp4"); } var notes = DecodePHXMO(objectsBuffer); - file.Dispose(); - // temp metadata.TryGetValue("ArtistLink", out Variant artistLink); metadata.TryGetValue("ArtistPlatform", out Variant artistPlatform); + + + byte[] hash = Misc.HashFiles([Path.Combine(path, "metadata.json"), Path.Combine(path, "objects.phxmo")]); + + DateTime metadataModified = File.GetLastWriteTime(Path.Combine(path, "metadata.json")); + DateTime objectsModified = File.GetLastWriteTime(Path.Combine(path, "objects.phxmo")); + + map = new( path, + BitConverter.ToString(hash).Replace("-", "").ToLower(), + metadataModified.ToString(), + objectsModified.ToString(), notes, (string)metadata["ID"], (string)metadata["Artist"], @@ -603,14 +699,97 @@ public static Map PHXM(string path) } catch (Exception exception) { - ToastNotification.Notify($"PHXM file corrupted", 2); + _ = ToastNotification.Notify($"PHXM folder corrupted", 2); Logger.Error(exception); throw; } - + return map; } + public static Map PHXM(string path) + { + + string mapDirectory = $"{Constants.USER_FOLDER}/maps"; + + // try + // { + // var file = ZipFile.OpenRead(path); + + // byte[] metaBuffer = getZipEntryBuffer(file, "metadata.json"); + // byte[] objectsBuffer = getZipEntryBuffer(file, "objects.phxmo"); + // byte[] audioBuffer = null; + // byte[] coverBuffer = null; + // byte[] videoBuffer = null; + + // var metadata = (Dictionary)Json.ParseString(Encoding.UTF8.GetString(metaBuffer)); + // FileParser objects = new(objectsBuffer); + + // if ((bool)metadata["HasAudio"]) + // { + // audioBuffer = getZipEntryBuffer(file, $"audio.{metadata["AudioExt"]}"); + // } + + // if ((bool)metadata["HasCover"]) + // { + // coverBuffer = getZipEntryBuffer(file, "cover.png"); + // } + + // if ((bool)metadata["HasVideo"]) + // { + // videoBuffer = getZipEntryBuffer(file, "video.mp4"); + // } + + // var notes = DecodePHXMO(objectsBuffer); + + // file.Dispose(); + + // // temp + // metadata.TryGetValue("ArtistLink", out Variant artistLink); + // metadata.TryGetValue("ArtistPlatform", out Variant artistPlatform); + + // map = new( + // path, + // notes, + // (string)metadata["ID"], + // (string)metadata["Artist"], + // (string)metadata["Title"], + // 0, + // (string[])metadata["Mappers"], + // (int)metadata["Difficulty"], + // (string)metadata["DifficultyName"], + // (int)metadata["Length"], + // audioBuffer, + // coverBuffer, + // videoBuffer, + // false, + // (string)artistLink ?? "", + // (string)artistPlatform ?? "" + // ); + // } + // catch (Exception exception) + // { + // ToastNotification.Notify($"PHXM file corrupted", 2); + // Logger.Error(exception); + // throw; + // } + + string extractedFolderName = Path.GetFileNameWithoutExtension(path); + string extractedFolderPath = Path.Combine(mapDirectory, extractedFolderName); + + if (Directory.Exists(extractedFolderPath)) + { + Directory.Delete(extractedFolderPath, true); // true = recursive + Map existingMap = DatabaseService.Connection.Table().FirstOrDefault(x => x.FolderPath == extractedFolderPath); + MapCache.RemoveMap(existingMap); + } + + ZipFile.ExtractToDirectory(path, extractedFolderPath); + File.Delete(path); + + return PHXMFolder(extractedFolderPath); + } + public static Note[] DecodePHXMO(string path) { FileParser objects = new(path); @@ -698,6 +877,9 @@ public static Map RHM(string path) map = new( path, + "", + "", + "", notes, null, artist ?? "", diff --git a/scripts/map/Replay.cs b/scripts/map/Replay.cs index 6da741ea..4e180900 100644 --- a/scripts/map/Replay.cs +++ b/scripts/map/Replay.cs @@ -168,13 +168,14 @@ public Replay(string path) MapID = FileBuffer.GetString((int)FileBuffer.GetUInt32()); MapNoteCount = FileBuffer.GetUInt64(); - MapFilePath = $"{Constants.USER_FOLDER}/maps/default/{MapID}.phxm"; + MapFilePath = $"{Constants.USER_FOLDER}/maps/{MapID}"; + MapFilePath = Path.GetFileNameWithoutExtension(MapFilePath); // just in case it tries to read a .phxm file - if (!File.Exists(MapFilePath)) + if (!Directory.Exists(MapFilePath)) { Valid = false; ToastNotification.Notify("Replay map not found", 2); - Logger.Log($"Replay map not found, path: {MapFilePath}.phxm"); + Logger.Log($"Replay map not found, path: {MapFilePath}"); return; } diff --git a/scripts/scenes/Game.cs b/scripts/scenes/Game.cs index 5858faaa..a507e2e6 100644 --- a/scripts/scenes/Game.cs +++ b/scripts/scenes/Game.cs @@ -170,7 +170,7 @@ public static void Play(Map map, double speed, double startFrom, CameraMode came StartQueued = true; - var parsedMap = MapParser.Decode(map.FilePath, Rhythia.AudioFilePath); + var parsedMap = MapParser.Decode(map.FolderPath, Rhythia.AudioFilePath); Attempt = new(parsedMap, speed, startFrom, cameraMode, mods, players, replays); if (!Attempt.IsReplay) @@ -189,7 +189,7 @@ public void Restart() Runner.Stop(false); var oldAttempt = Attempt; - var map = MapParser.Decode(oldAttempt.Map.FilePath, Rhythia.AudioFilePath); + var map = MapParser.Decode(oldAttempt.Map.FolderPath, Rhythia.AudioFilePath); Attempt = new(map, oldAttempt.Speed, oldAttempt.StartFrom, oldAttempt.CameraMode, oldAttempt.Modifiers, oldAttempt.Players, oldAttempt.Replays); SceneManager.ReloadCurrentScene(); diff --git a/scripts/scenes/Results.cs b/scripts/scenes/Results.cs index e775065f..916e4664 100644 --- a/scripts/scenes/Results.cs +++ b/scripts/scenes/Results.cs @@ -173,7 +173,7 @@ public void Replay() { var attempt = Game.Attempt; - Map map = MapParser.Decode(attempt.Map.FilePath); + Map map = MapParser.Decode(attempt.Map.FolderPath); map.Ephemeral = attempt.Map.Ephemeral; SoundManager.Song.Stop(); diff --git a/scripts/ui/menu/play/MapInfoContainer.cs b/scripts/ui/menu/play/MapInfoContainer.cs index 8611ee9b..2e11feae 100644 --- a/scripts/ui/menu/play/MapInfoContainer.cs +++ b/scripts/ui/menu/play/MapInfoContainer.cs @@ -44,6 +44,9 @@ public partial class MapInfoContainer : Panel, ISkinnable private LinkPopupButton artistLink; private string artistLinkFormat; + [Export] + private Button exportButton; + [Export] private Button favoriteButton; @@ -123,6 +126,20 @@ public override void _Ready() artistLinkFormat = artistLink.Text; outlineMaterial = info.GetNode("Outline").Material as ShaderMaterial; + exportButton.Pressed += () => + { + string exportPath = $"{Constants.USER_FOLDER}/export/"; + string exportFilePath = Path.Combine(exportPath, $"{Map.Name}.phxm"); + + _ = ToastNotification.Notify($"Exporting to {exportFilePath}", 1); + MapParser.ExportEncode(Map); + + _ = ToastNotification.Notify($"Done! Opening export...", 0); + + OS.ShellShowInFileManager(exportFilePath); + + }; + favoriteButton.Pressed += () => { Map.Favorite = !Map.Favorite; @@ -144,17 +161,17 @@ public override void _Ready() // MapManager.InsertVideo(Map, file); //}; - copyButton.Pressed += () => - { - copyDialog.Popup(); + // copyButton.Pressed += () => + // { + // copyDialog.Popup(); - }; + // }; - copyDialog.FileSelected += (path) => - { - File.Copy(Map.FilePath, path); - _ = ToastNotification.Notify($"Copied to {path}"); - }; + // copyDialog.FileSelected += (path) => + // { + // File.Copy(Map.FolderPath, path); + // _ = ToastNotification.Notify($"Copied to {path}"); + // }; deleteButton.Pressed += () => { diff --git a/scripts/util/Misc.cs b/scripts/util/Misc.cs index 38a1716c..3f882c06 100644 --- a/scripts/util/Misc.cs +++ b/scripts/util/Misc.cs @@ -1,4 +1,8 @@ +using System; using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; using Godot; namespace Util; @@ -61,6 +65,21 @@ public static void CopyProperties(Node node, Node reference) } } + public static byte[] HashFiles(string[] paths) + { + using var md5 = MD5.Create(); + + foreach (string path in paths) // we do not need to order the paths since it will always be the same -fog + { + byte[] fileData = File.ReadAllBytes(path); + md5.TransformBlock(fileData, 0, fileData.Length, null, 0); + } + + md5.TransformFinalBlock(Array.Empty(), 0 ,0); + + return md5.Hash; + } + public static void CopyReference(Node node, Node reference) { CopyProperties(node, reference); diff --git a/user/skins/default/ui/buttons/export.png b/user/skins/default/ui/buttons/export.png new file mode 100644 index 00000000..4004e8ad Binary files /dev/null and b/user/skins/default/ui/buttons/export.png differ diff --git a/user/skins/default/ui/buttons/export.png.import b/user/skins/default/ui/buttons/export.png.import new file mode 100644 index 00000000..33dbb9a9 --- /dev/null +++ b/user/skins/default/ui/buttons/export.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkpbociv8fgfd" +path="res://.godot/imported/export.png-9d48d4339a04990c70115e465c24c745.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://user/skins/default/ui/buttons/export.png" +dest_files=["res://.godot/imported/export.png-9d48d4339a04990c70115e465c24c745.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1