diff --git a/Rhythia.csproj b/Rhythia.csproj
index 94da00f6..ced6fcc4 100644
--- a/Rhythia.csproj
+++ b/Rhythia.csproj
@@ -20,4 +20,3 @@
-
diff --git a/scenes/main.tscn b/scenes/main.tscn
index 4a9b0bfe..317c889a 100644
--- a/scenes/main.tscn
+++ b/scenes/main.tscn
@@ -130,7 +130,7 @@ handle_input_locally = false
size = Vector2i(1280, 720)
render_target_update_mode = 4
-[node name="Settings" type="ColorRect" parent="." unique_id=1393582030]
+[node name="Settings" type="ColorRect" parent="." unique_id=1393582030 node_paths=PackedStringArray("ImportNightlyDialog")]
visible = false
z_index = 1
anchors_preset = 15
@@ -141,6 +141,7 @@ grow_vertical = 2
mouse_filter = 2
color = Color(0, 0, 0, 0.501961)
script = ExtResource("2_o6xl0")
+ImportNightlyDialog = NodePath("ImportNightlyDialog")
[node name="Hide" type="Button" parent="Settings" unique_id=1153372742]
self_modulate = Color(1, 1, 1, 0)
@@ -429,6 +430,13 @@ grow_vertical = 2
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_popqt")
+[node name="ImportNightlyDialog" type="FileDialog" parent="Settings" unique_id=1831752407]
+title = "Open a File"
+file_mode = 0
+access = 2
+filters = PackedStringArray("*.json")
+use_native_dialog = true
+
[node name="Volume" type="Panel" parent="." unique_id=1219254705]
modulate = Color(1, 1, 1, 0)
z_index = 1
diff --git a/scenes/main_menu.tscn b/scenes/main_menu.tscn
index 8801bb64..46331bb9 100644
--- a/scenes/main_menu.tscn
+++ b/scenes/main_menu.tscn
@@ -1730,6 +1730,15 @@ bbcode_enabled = true
text = "[font_size=20]noobour
[color=0f81b7][font_size=16]Contributor"
+[node name="xwalfie" type="RichTextLabel" parent="Menus/Extras/Credits/ScrollContainer/VBoxContainer" unique_id=427847284]
+modulate = Color(0.2627451, 1, 0.9529412, 1)
+layout_mode = 2
+bbcode_enabled = true
+text = "[font_size=20]xwalfie
+[color=ffffffbb][font_size=16]Contributor"
+fit_content = true
+horizontal_alignment = 1
+
[node name="Outline" type="Panel" parent="Menus/Extras/Credits" unique_id=239136532]
material = SubResource("ShaderMaterial_yxnar")
layout_mode = 1
diff --git a/scripts/Constants.cs b/scripts/Constants.cs
index 580934f3..7f0a19f6 100644
--- a/scripts/Constants.cs
+++ b/scripts/Constants.cs
@@ -10,6 +10,8 @@ public partial class Constants : Node
public static readonly string USER_FOLDER = OS.GetUserDataDir();
+ public static readonly string NIGHTLY_FOLDER = $"{Path.GetDirectoryName(USER_FOLDER)}/SoundSpacePlus";
+
public static readonly string DEFAULT_MAP_EXT = "phxm";
public static readonly bool TEMP_MAP_MODE = false;//OS.GetCmdlineArgs().Length > 0;
diff --git a/scripts/SoundManager.cs b/scripts/SoundManager.cs
index 9eb59133..e0fcea83 100644
--- a/scripts/SoundManager.cs
+++ b/scripts/SoundManager.cs
@@ -375,6 +375,12 @@ public static float ComputeVolumeDb(float volume, float master, float range)
return (float)(-80 + range * Math.Pow(volume / 100, 0.1) * Math.Pow(master / 100, 0.1));
}
+ public static float ComputeVolumeFromDb(float db, float master, float range)
+ {
+ if (float.IsNegativeInfinity(db) || master <= 0) return 0;
+ return (float)Math.Clamp(100 * Math.Pow((db + 80) / (range * Math.Pow(master / 100, 0.1)), 10), 0, 100);
+ }
+
public static void UpdateVolume()
{
var settings = SettingsManager.Instance.Settings;
diff --git a/scripts/database/settings/SettingsProfile.cs b/scripts/database/settings/SettingsProfile.cs
index dd085036..b4c3e220 100644
--- a/scripts/database/settings/SettingsProfile.cs
+++ b/scripts/database/settings/SettingsProfile.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using Godot;
public partial class SettingsProfile
@@ -342,11 +343,29 @@ public partial class SettingsProfile
///
public SettingsItem DisplayFPS { get; private set; }
- // [Order]
+ [Order]
///
/// Import settings from previous (nightly) version
///
- // public SettingsItem RhythiaImport { get; private set; }
+ public SettingsItem ImportNightlyProfile { get; private set; }
+
+ [Order]
+ ///
+ /// File dialog for the nightly import
+ ///
+ public SettingsItem NightlyImportDialog { get; private set; }
+
+ [Order]
+ ///
+ /// Imports meshes from the nightly folder
+ ///
+ public SettingsItem ImportNightlyMeshes { get; private set; }
+
+ [Order]
+ ///
+ /// Imports colorsets from the nightly folder
+ ///
+ public SettingsItem ImportNightlyColorsets { get; private set; }
[Order]
///
@@ -1030,18 +1049,72 @@ public SettingsProfile()
#region Other
- // RhythiaImport = new(default)
- // {
- // Id = "RhythiaImport",
- // Title = "Import Nightly Settings",
- // Description = "Imports settings from the nightly client",
- // Section = SettingsSection.Other,
- // Buttons =
- // [
- // new() { Title = "Import", Description = "", OnPressed = () => { } }
- // ],
- // SaveToDisk = false,
- // };
+ ImportNightlyProfile = new(default)
+ {
+ Id = "ImportNightlyProfile",
+ Title = "Import Nightly Settings",
+ Description = "Imports settings from the nightly client",
+ Section = SettingsSection.Other,
+ Buttons =
+ [
+ new() { Title = "Import", Description = "Automatically import settings from nightly", OnPressed = () => {
+
+ if (Directory.Exists(Constants.NIGHTLY_FOLDER)) {
+ ImportFromNightlySettings($"{Constants.NIGHTLY_FOLDER}/settings.json");
+ }
+ } }
+ ],
+ SaveToDisk = false,
+ };
+
+ NightlyImportDialog = new(default)
+ {
+ Id = "NightlyImportDialog",
+ Title = "", // this has no title because its supposed to be a button belonging to the field above
+ Description = "",
+ Section = SettingsSection.Other,
+ Buttons =
+ [
+ new() { Title = "Choose file", Description = "Import manually from a nightly settings file", OnPressed = () => {
+ SettingsMenu.Instance.ImportNightlyDialog.PopupCentered();
+ }}
+ ],
+ SaveToDisk = false,
+ };
+
+ ImportNightlyMeshes = new(default)
+ {
+ Id = "ImportNightlyMeshes",
+ Title = "Import Nightly Meshes",
+ Description = "Imports meshes from the nightly folder",
+ Section = SettingsSection.Other,
+ Buttons =
+ [
+ new() { Title = "Import Nightly Meshes", Description = "Imports meshes from the nightly folder", OnPressed = () => {
+ importMeshesFromNightly();
+ SettingsManager.Instance.Settings.NoteMesh.List.Values = getAvailableMeshes();
+ SettingsMenu.Instance.RefreshList(SettingsManager.Instance.Settings.NoteMesh);
+ }}
+ ],
+ SaveToDisk = false,
+ };
+
+ ImportNightlyColorsets = new(default)
+ {
+ Id = "ImportNightlyColorsets",
+ Title = "Import Nightly Colorsets",
+ Description = "Imports colorsets from the nightly folder",
+ Section = SettingsSection.Other,
+ Buttons =
+ [
+ new() { Title = "Import Nightly Colorsets", Description = "Imports colorsets from the nightly folder", OnPressed = () => {
+ importColorsetsFromNightly();
+ SettingsManager.Load();
+ SettingsMenu.Instance.RefreshList(SettingsManager.Instance.Settings.NoteColors);
+ }}
+ ],
+ SaveToDisk = false,
+ };
DisplayFPS = new(true)
{
@@ -1148,4 +1221,199 @@ private static List getAvailableMeshes()
return meshes;
}
+
+ public static void ImportFromNightlySettings(string settingsPath)
+ {
+ string nightlySettings = File.Exists(settingsPath) ? File.ReadAllText(settingsPath) : null;
+
+ if (nightlySettings == null)
+ {
+ ToastNotification.Notify("Nightly settings not found, choose the settings file manually.");
+ return;
+ }
+
+ using JsonDocument json = JsonDocument.Parse(nightlySettings);
+ JsonElement root = json.RootElement;
+
+ T? getSetting(string key) where T : struct
+ {
+ if (root.TryGetProperty(key, out JsonElement element))
+ {
+ return element.Deserialize();
+ }
+
+ return null;
+ }
+
+ // needs a separate helper for strings due to the struct constraint in getSetting()
+ string? getStringSetting(string key)
+ {
+ if (root.TryGetProperty(key, out JsonElement element))
+ {
+ return element.GetString();
+ }
+
+ return null;
+ }
+
+ double importVolume(string nightlyKey, double range)
+ {
+ double? channelDb = getSetting(nightlyKey);
+
+ if (channelDb == null)
+ {
+ return 50;
+ }
+
+ double masterDb = getSetting("master_volume") ?? 20 * Math.Log10(0.5);
+ double totalDb = Math.Clamp(masterDb + channelDb.Value, -80, 0);
+
+ return SoundManager.ComputeVolumeFromDb((float)totalDb, 100, (float)range);
+ }
+
+ SettingsProfile nightlyProfile = new SettingsProfile();
+
+ Dictionary> conversions = new()
+ {
+ // sensitivity scales with fov but in nightly it doesnt
+ ["Sensitivity"] = () => getSetting("sensitivity") * 2.16 * (70 / (getSetting("fov") ?? 70)) ?? nightlyProfile.Sensitivity,
+ ["AbsoluteSensitivity"] = () => getSetting("absolute_scale") ?? nightlyProfile.AbsoluteSensitivity,
+ ["AbsoluteInput"] = () => getSetting("absolute_mode") ?? nightlyProfile.AbsoluteInput,
+ ["CursorDrift"] = () => getSetting("enable_drift_cursor") ?? nightlyProfile.CursorDrift,
+ ["ApproachRate"] = () => getSetting("approach_rate") ?? nightlyProfile.ApproachRate,
+ ["ApproachDistance"] = () => getSetting("spawn_distance") ?? nightlyProfile.ApproachDistance,
+ ["Pushback"] = () => getSetting("do_note_pushback") ?? nightlyProfile.Pushback,
+ ["CameraParallax"] = () => getSetting("parallax") * 0.025 ?? nightlyProfile.CameraParallax,
+ ["HUDParallax"] = () => getSetting("ui_parallax") * 0.025 ?? nightlyProfile.HUDParallax,
+ ["FoV"] = () => getSetting("fov") ?? nightlyProfile.FoV,
+ ["Colors"] = () => getStringSetting("selected_colorset") ?? nightlyProfile.NoteColors,
+ ["NoteMesh"] = () => getStringSetting("selected_mesh") ?? nightlyProfile.NoteMesh,
+ ["NoteSize"] = () => getSetting("note_size") * 0.875 ?? nightlyProfile.NoteSize,
+ ["NoteOpacity"] = () => getSetting("note_opacity") ?? nightlyProfile.NoteOpacity,
+ ["CursorScale"] = () => getSetting("cursor_scale") ?? nightlyProfile.CursorScale,
+ ["CursorRotation"] = () => getSetting("cursor_spin") ?? nightlyProfile.CursorRotation,
+ ["CursorTrail"] = () => getSetting("cursor_trail") ?? nightlyProfile.CursorTrail,
+ ["TrailTime"] = () => getSetting("trail_time") ?? nightlyProfile.TrailTime,
+ ["TrailDetail"] = () => getSetting("trail_detail") ?? nightlyProfile.TrailDetail,
+ ["SimpleHUD"] = () => getSetting("simple_hud") ?? nightlyProfile.SimpleHUD,
+ ["HitPopups"] = () => getSetting("score_popup") ?? nightlyProfile.HitPopups,
+ ["MissPopups"] = () => getSetting("show_miss_effect") ?? nightlyProfile.MissPopups,
+ ["Fullscreen"] = () => getSetting("window_fullscreen") ?? nightlyProfile.Fullscreen,
+ ["FPS"] = () => getSetting("target_fps") ?? nightlyProfile.FPS,
+ ["VolumeMaster"] = () => 100,
+ ["VolumeMusic"] = () => importVolume("music_volume", 70),
+ ["VolumeHitSound"] = () => importVolume("hit_volume", 80),
+ ["VolumeMissSound"] = () => importVolume("miss_volume", 80),
+ ["VolumeSFX"] = () => importVolume("fail_volume", 80),
+ ["EnableHitSound"] = () => getSetting("play_hit_snd") ?? nightlyProfile.EnableHitSound,
+ ["EnableMissSound"] = () => getSetting("play_miss_snd") ?? nightlyProfile.EnableMissSound,
+ ["EnableMenuMusic"] = () => getSetting("play_menu_music") ?? nightlyProfile.EnableMenuMusic,
+ ["AutoplayJukebox"] = () => getSetting("auto_preview_song") ?? nightlyProfile.AutoplayJukebox,
+ ["LocalOffset"] = () => getSetting("music_offset") ?? nightlyProfile.LocalOffset,
+ ["RecordReplays"] = () => getSetting("record_replays") ?? nightlyProfile.RecordReplays,
+ };
+
+ var settingsById = typeof(SettingsProfile).GetProperties()
+ .Where(p => typeof(ISettingsItem).IsAssignableFrom(p.PropertyType))
+ .Select(p => (ISettingsItem)p.GetValue(nightlyProfile))
+ .ToDictionary(item => item.Id);
+
+ foreach (var (id, convert) in conversions)
+ {
+ settingsById[id].SetVariant(convert());
+ }
+
+ // prevents overriding existing 'nightly' profile
+ string getProfileName(string baseName)
+ {
+ string profilesDir = $"{Constants.USER_FOLDER}/profiles";
+ Directory.CreateDirectory(profilesDir);
+
+ string name = baseName;
+ int suffix = 0;
+
+ while (File.Exists($"{profilesDir}/{name}.json"))
+ {
+ suffix++;
+ name = $"{baseName}-{suffix}";
+ }
+
+ return name;
+ }
+
+ string profileName = getProfileName("nightly");
+ string profileJson = SettingsProfileConverter.Serialize(nightlyProfile);
+ File.WriteAllText($"{Constants.USER_FOLDER}/profiles/{profileName}.json", profileJson);
+
+ SettingsManager.SetCurrentProfile(profileName);
+ SettingsManager.Load();
+ SettingsMenu.Instance.UpdateProfileSelection();
+
+ ToastNotification.Notify($"Created profile '{profileName}'");
+ }
+
+ private static void importColorsetsFromNightly()
+ {
+ string nightlyColorsetsDir = $"{Constants.NIGHTLY_FOLDER}/colorsets";
+ string colorsetsDir = $"{Constants.USER_FOLDER}/colorsets";
+ int importedCount = 0;
+
+ if (!Directory.Exists(nightlyColorsetsDir))
+ {
+ ToastNotification.Notify("The nightly colorsets folder doesn't exist");
+ return;
+ }
+
+ Directory.CreateDirectory(colorsetsDir);
+
+ foreach (string file in Directory.GetFiles(nightlyColorsetsDir))
+ {
+ string fileName = Path.GetFileName(file);
+ string destinationPath = $"{colorsetsDir}/{fileName}";
+
+ if (!File.Exists(destinationPath))
+ {
+ File.Copy(file, destinationPath);
+ importedCount++;
+ }
+ }
+
+ ToastNotification.Notify($"Imported {importedCount} colorsets from nightly");
+ }
+
+ private static void importMeshesFromNightly()
+ {
+ string nightlyMeshesDir = $"{Constants.NIGHTLY_FOLDER}/meshes";
+ string meshesDir = $"{Constants.USER_FOLDER}/meshes";
+ int importedCount = 0;
+
+ if (!Directory.Exists(nightlyMeshesDir))
+ {
+ ToastNotification.Notify("The nightly meshes folder doesn't exist");
+ return;
+ }
+
+ Directory.CreateDirectory(meshesDir);
+
+ foreach (string file in Directory.GetFiles(nightlyMeshesDir, "*.obj"))
+ {
+ string fileName = Path.GetFileName(file);
+ string destinationPath = $"{meshesDir}/{fileName}";
+
+ if (!File.Exists(destinationPath))
+ {
+ string mtlFile = Path.ChangeExtension(file, ".mtl");
+ string mtlDestinationPath = $"{meshesDir}/{Path.GetFileName(mtlFile)}";
+
+ File.Copy(file, destinationPath);
+ if (File.Exists(mtlFile))
+ {
+ File.Copy(mtlFile, mtlDestinationPath);
+ }
+ importedCount++;
+ }
+ }
+
+ ToastNotification.Notify($"Imported {importedCount} meshes from nightly");
+ }
}
diff --git a/scripts/ui/SettingsMenu.cs b/scripts/ui/SettingsMenu.cs
index 1b89c2c5..e013da89 100644
--- a/scripts/ui/SettingsMenu.cs
+++ b/scripts/ui/SettingsMenu.cs
@@ -22,6 +22,8 @@ public partial class SettingsMenu : ColorRect
private ScrollContainer selectedCategory;
+ [Export] public FileDialog ImportNightlyDialog;
+
public override void _Ready()
{
Instance = this;
@@ -53,7 +55,7 @@ public override void _Ready()
SettingsManager.SetCurrentProfile(profile);
SettingsManager.Reload();
- updateProfileSelection();
+ UpdateProfileSelection();
};
profilesButton.ItemSelected += (index) =>
@@ -67,7 +69,7 @@ public override void _Ready()
SettingsManager.Load();
};
- updateProfileSelection();
+ UpdateProfileSelection();
Panel settingTemplate = categoryTemplate.GetNode("Container").GetNode("SettingTemplate");
CheckButton checkButtonTemplate = settingTemplate.GetNode("CheckButton");
@@ -189,6 +191,7 @@ public override void _Ready()
HideMenu();
hideButton.Pressed += HideMenu;
+ ImportNightlyDialog.FileSelected += SettingsProfile.ImportFromNightlySettings;
}
// Adding GetViewport().SetInputAsHandled() will prevent the Quit popup from appearing when clicking ESC in settings
public override void _Input(InputEvent @event)
@@ -248,7 +251,7 @@ public void SelectCategory(ScrollContainer category)
sidebar.GetNode(new(selectedCategory.Name)).Color = Color.Color8(255, 255, 255, 8);
}
- private void updateProfileSelection()
+ public void UpdateProfileSelection()
{
// skip default
for (int i = 1; i < profilesButton.ItemCount; i++)
@@ -431,4 +434,15 @@ private void setupButton(SettingsButton setting, Button button)
setting.OnPressed?.Invoke();
};
}
+
+ public void RefreshList(ISettingsItem setting)
+ {
+ OptionButton optionButton = settingPanels[setting.Id].GetNode("OptionButton");
+ optionButton.Clear();
+
+ foreach (Variant item in setting.List.Values)
+ {
+ optionButton.AddItem(item.AsString());
+ }
+ }
}