Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,24 @@ public override QuestWorkValue Read(ref Utf8JsonReader reader, Type typeToConver
throw new JsonException();
}

public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options)
{
if (value.High != null && value.Low != null && value.Mode == EQuestWorkMode.Bitwise)
writer.WriteNumberValue((byte)((value.High.Value << 4) + value.Low.Value));
else
{
writer.WriteStartObject();
if (value.High != null)
writer.WriteNumber(nameof(QuestWorkValue.High), value.High.Value);
if (value.Low != null)
writer.WriteNumber(nameof(QuestWorkValue.Low), value.Low.Value);
if (value.Mode != EQuestWorkMode.Bitwise)
{
writer.WritePropertyName(nameof(QuestWorkValue.Mode));
new QuestWorkModeConverter().Write(writer, value.Mode, options);
}

writer.WriteEndObject();
}
}
}
2 changes: 2 additions & 0 deletions Questionable.Model/Questing/DialogueChoice.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Questionable.Model.Common;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;

Expand All @@ -11,6 +12,7 @@ public sealed class DialogueChoice
[JsonConverter(typeof(ExcelRefConverter))]
public ExcelRef? Prompt { get; set; }

[DefaultTrue]
public bool Yes { get; set; } = true;

[JsonConverter(typeof(ExcelRefConverter))]
Expand Down
31 changes: 31 additions & 0 deletions Questionable.Tests/Serialization/QuestPathSaveStabilityTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text.Json;
using Questionable.Model.Questing;
using Questionable.Tests.TestData;
using Questionable.Utils;
using Xunit;

namespace Questionable.Tests.Serialization;

public sealed class QuestPathSaveStabilityTest
{
[Theory]
[ClassData(typeof(EmbeddedQuestLoader))]
public void SavingAQuestPathProducesIdenticalJson(EmbeddedQuest embedded)
{
QuestRoot original;
using (var stream = embedded.OpenStream())
{
original = JsonSerializer.Deserialize<QuestRoot>(stream)
?? throw new Xunit.Sdk.XunitException(
$"'{embedded.ManifestName}' deserialized to a null QuestRoot.");
}

string once = JsonSerializer.Serialize(original, JsonOptions.Default);
QuestRoot reloaded = JsonSerializer.Deserialize<QuestRoot>(once)
?? throw new Xunit.Sdk.XunitException(
$"Re-deserializing '{embedded.ManifestName}' returned null.");
string twice = JsonSerializer.Serialize(reloaded, JsonOptions.Default);

Assert.Equal(once, twice);
}
}
31 changes: 31 additions & 0 deletions Questionable/Controller/QuestRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,37 @@ public static (bool, FileInfo?, string) CreatePath(QuestInfo info, Quest? quest
return (true, file, $"File created{(dryrun ? " (dry run)" : "")}");
}

public static (bool Success, string Message) SaveUserPath(QuestInfo info, QuestRoot root)
{
string directory = Path.Combine(Svc.PluginInterface.GetPluginConfigDirectory(), "Quests");
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, GetFilename(info));

JsonObject serialized = (JsonObject)JsonSerializer.SerializeToNode(root, JsonOptions.Default)!;
JsonObject withSchema = new()
{
{
"$schema",
"https://qstxiv.github.io/schema/quest-v1.json"
}
};
foreach ((string key, JsonNode? value) in serialized)
withSchema.Add(key, value?.DeepClone());

using (FileStream stream = new(path, FileMode.Create))
using (Utf8JsonWriter writer = new(stream, new()
{
Encoder = JsonOptions.Default.Encoder,
Indented = JsonOptions.Default.WriteIndented
}))
{
withSchema.WriteTo(writer, JsonOptions.Default);
}

Svc.Log.Information($"Saved user quest path to {path}");
return (true, path);
}

public static string OpenEditorDescription = _L("Clicking this button writes the quest path to a file and opens it in your default text editor.") +
_L("After making a change, click Reload Data below.") + "\n" +
_L("To revert to the official version, delete the file and click Reload Data again.") + "\n" +
Expand Down
2 changes: 2 additions & 0 deletions Questionable/DalamudInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public DalamudInitializer(
QuestValidationWindow questValidationWindow,
JournalProgressWindow journalProgressWindow,
PriorityWindow priorityWindow,
PathEditorWindow pathEditorWindow,
IChatGui chatGui,
IToastGui toastGui,
Configuration configuration,
Expand Down Expand Up @@ -65,6 +66,7 @@ public DalamudInitializer(
_windowSystem.AddWindow(questValidationWindow);
_windowSystem.AddWindow(journalProgressWindow);
_windowSystem.AddWindow(priorityWindow);
_windowSystem.AddWindow(pathEditorWindow);

_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += ToggleQuestWindow;
Expand Down
4 changes: 4 additions & 0 deletions Questionable/QuestionablePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ private static void AddWindows(ServiceCollection serviceCollection)
serviceCollection.AddSingleton<QuestValidationWindow>();
serviceCollection.AddSingleton<JournalProgressWindow>();
serviceCollection.AddSingleton<PriorityWindow>();
serviceCollection.AddSingleton<Windows.PathEditorComponents.PathEditorSession>();
serviceCollection.AddSingleton<Windows.PathEditorComponents.StepFormComponent>();
serviceCollection.AddSingleton<Windows.PathEditorComponents.StepCaptureComponent>();
serviceCollection.AddSingleton<PathEditorWindow>();

serviceCollection.AddSingleton<GeneralConfigComponent>();
serviceCollection.AddSingleton<PluginConfigComponent>();
Expand Down
79 changes: 39 additions & 40 deletions Questionable/Utils/ImGuiComponentsLocal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,54 +96,53 @@ internal static bool DrawSearchableCombo<T>(
var size = ImGui.GetWindowContentRegionMax();
ImGui.SetNextItemWidth(size.X / 2);
}
if (ImGui.BeginCombo($"{(!labelAsPreview ? label : "")}##SearchableCombo:{Path.GetFileName(file)}:{line}", preview, ImGuiComboFlags.HeightLarge))
using var combo = ImRaii.Combo($"{(!labelAsPreview ? label : "")}##SearchableCombo:{Path.GetFileName(file)}:{line}", preview, ImGuiComboFlags.HeightLarge);
if (!combo)
return false;

ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.IsWindowAppearing())
ImGui.SetKeyboardFocusHere();
ImGui.InputTextWithHint("##filter", "Search...", ref searchString, 256);

// The option list lives in its own fixed-height scrollable child so the search box above
// stays pinned and visible; SetItemDefaultFocus() then scrolls the child, not the popup.
int visibleRows = Math.Clamp(labels.Length, 1, 12);
var listSize = ImGui.GetContentRegionAvail() with { Y = ImGui.GetTextLineHeightWithSpacing() * visibleRows };
using (var child = ImRaii.Child("##searchableComboList", listSize))
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.IsWindowAppearing())
ImGui.SetKeyboardFocusHere();
ImGui.InputTextWithHint("##filter", "Search...", ref searchString, 256);

// The option list lives in its own fixed-height scrollable child so the search box above
// stays pinned and visible; SetItemDefaultFocus() then scrolls the child, not the popup.
int visibleRows = Math.Clamp(labels.Length, 1, 12);
var listSize = ImGui.GetContentRegionAvail() with { Y = ImGui.GetTextLineHeightWithSpacing() * visibleRows };
using (var child = ImRaii.Child("##searchableComboList", listSize))
if (child)
{
if (child)
for (int i = 0; i < labels.Length; i++)
{
for (int i = 0; i < labels.Length; i++)
if (!string.IsNullOrEmpty(searchString) &&
!labels[i].Contains(searchString, StringComparison.CurrentCultureIgnoreCase))
continue;
if (labels[i].StartsWith("##D"))
{
if (!string.IsNullOrEmpty(searchString) &&
!labels[i].Contains(searchString, StringComparison.CurrentCultureIgnoreCase))
continue;
if (labels[i].StartsWith("##D"))
{
ImGui.TextDisabled(labels[i].Substring(3));
continue;
}
if (labels[i].StartsWith("##S"))
{
ImGui.Separator();
continue;
}

bool isSelected = i == index;
if (ImGui.Selectable(labels[i], isSelected))
{
selected = values[i];
searchString = string.Empty;
}

if (isSelected)
ImGui.SetItemDefaultFocus();
ImGui.TextDisabled(labels[i].Substring(3));
continue;
}
if (labels[i].StartsWith("##S"))
{
ImGui.Separator();
continue;
}

bool isSelected = i == index;
if (ImGui.Selectable(labels[i], isSelected))
{
selected = values[i];
searchString = string.Empty;
}

if (isSelected)
ImGui.SetItemDefaultFocus();
}
}

ImGui.EndCombo();
return true;
}
return false;

return true;
}

public static void HelpMarker(string helpText, string[]? bullets) => HelpMarker(helpText, FontAwesomeIcon.InfoCircle, bullets: bullets);
Expand Down
7 changes: 7 additions & 0 deletions Questionable/Windows/Common/LWindow.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Dalamud.Bindings.ImGui;
using Questionable.Windows.Common.Ui;
namespace Questionable.Windows.Common;

public abstract class LWindow(string windowName, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false) : Window(windowName, flags, forceMainWindow)
{
private bool _initializedConfig;
private bool _wasCollapsedLastFrame;
private QstTheme.WindowStyleScope? _windowStyle;

protected bool ClickedHeaderLastFrame { get; private set; }
protected bool ClickedHeaderCurrentFrame { get; private set; }
Expand Down Expand Up @@ -98,6 +100,8 @@ public override void OnOpen()

public override void PreDraw()
{
_windowStyle = QstTheme.PushWindowStyle();

if (!_initializedConfig)
LoadWindowConfig();

Expand All @@ -124,6 +128,9 @@ public sealed override void Draw()

public override void PostDraw()
{
_windowStyle?.Dispose();
_windowStyle = null;

base.PostDraw();

if (_initializedConfig)
Expand Down
102 changes: 102 additions & 0 deletions Questionable/Windows/Common/Ui/QstTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;

namespace Questionable.Windows.Common.Ui;

internal static class QstTheme
{
public static readonly Vector4 Accent = Rgb(255, 148, 68);
public static readonly Vector4 AccentActive = Rgb(255, 167, 94);
public static readonly Vector4 Danger = Rgb(255, 81, 110);
public static readonly Vector4 Amber = Rgb(255, 176, 102);
public static readonly Vector4 Success = Rgb(126, 212, 145);
public static readonly Vector4 Info = Rgb(111, 174, 230);
public static readonly Vector4 Special = Rgb(181, 144, 232);
public static readonly Vector4 Text = Rgb(224, 224, 224);
public static readonly Vector4 TextMuted = Rgb(156, 163, 175);
public static readonly Vector4 TextFaint = Rgb(117, 123, 134);

public static readonly Vector4 PanelBg = Rgba(26, 26, 26, 0.96f);
public static readonly Vector4 PanelDark = Rgb(14, 14, 14);
public static readonly Vector4 InputBg = Rgb(19, 19, 19);
public static readonly Vector4 Raised = Rgb(38, 38, 38);
public static readonly Vector4 RaisedHovered = Rgb(48, 48, 48);
public static readonly Vector4 RaisedActive = Rgb(58, 58, 58);
public static readonly Vector4 Edge = Rgb(46, 46, 46);

public static Vector4 WithAlpha(Vector4 color, float alpha) => color with { W = alpha };

public static uint ToU32(Vector4 color) => ImGui.ColorConvertFloat4ToU32(color);

public static WindowStyleScope PushWindowStyle() => new();

internal sealed class WindowStyleScope : IDisposable
{
private readonly ImRaii.ColorDisposable _colors;
private readonly ImRaii.StyleDisposable _styles;

internal WindowStyleScope()
{
_colors = ImRaii.PushColor(ImGuiCol.WindowBg, PanelBg)
.Push(ImGuiCol.PopupBg, Rgba(27, 27, 27, 0.98f))
.Push(ImGuiCol.Border, Edge)
.Push(ImGuiCol.Separator, Edge)
.Push(ImGuiCol.FrameBg, InputBg)
.Push(ImGuiCol.FrameBgHovered, Rgb(31, 31, 31))
.Push(ImGuiCol.FrameBgActive, Raised)
.Push(ImGuiCol.TitleBg, PanelDark)
.Push(ImGuiCol.TitleBgActive, Rgb(26, 26, 26))
.Push(ImGuiCol.TitleBgCollapsed, Rgba(14, 14, 14, 0.8f))
.Push(ImGuiCol.Button, Raised)
.Push(ImGuiCol.ButtonHovered, RaisedHovered)
.Push(ImGuiCol.ButtonActive, RaisedActive)
.Push(ImGuiCol.Header, Raised)
.Push(ImGuiCol.HeaderHovered, RaisedHovered)
.Push(ImGuiCol.HeaderActive, RaisedActive)
.Push(ImGuiCol.Tab, Rgb(26, 26, 26))
.Push(ImGuiCol.TabHovered, RaisedHovered)
.Push(ImGuiCol.TabActive, Rgb(43, 43, 43))
.Push(ImGuiCol.TabUnfocused, Rgb(26, 26, 26))
.Push(ImGuiCol.TabUnfocusedActive, Rgb(36, 36, 36))
.Push(ImGuiCol.CheckMark, Accent)
.Push(ImGuiCol.SliderGrab, Accent)
.Push(ImGuiCol.SliderGrabActive, AccentActive)
.Push(ImGuiCol.ScrollbarBg, Rgba(19, 19, 19, 0.5f))
.Push(ImGuiCol.ScrollbarGrab, Rgb(52, 52, 52))
.Push(ImGuiCol.ScrollbarGrabHovered, Rgb(70, 70, 70))
.Push(ImGuiCol.ScrollbarGrabActive, Rgb(86, 86, 86))
.Push(ImGuiCol.TextSelectedBg, WithAlpha(Accent, 0.35f))
.Push(ImGuiCol.Text, Text)
.Push(ImGuiCol.TextDisabled, TextFaint);

_styles = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 8f)
.Push(ImGuiStyleVar.ChildRounding, 6f)
.Push(ImGuiStyleVar.FrameRounding, 5f)
.Push(ImGuiStyleVar.PopupRounding, 6f)
.Push(ImGuiStyleVar.GrabRounding, 4f)
.Push(ImGuiStyleVar.TabRounding, 5f)
.Push(ImGuiStyleVar.ScrollbarRounding, 6f)
.Push(ImGuiStyleVar.ScrollbarSize, 10f)
.Push(ImGuiStyleVar.WindowBorderSize, 1f)
.Push(ImGuiStyleVar.WindowPadding, new Vector2(10, 9))
.Push(ImGuiStyleVar.FramePadding, new Vector2(6, 4))
.Push(ImGuiStyleVar.ItemSpacing, new Vector2(7, 5));
}

public void Dispose()
{
_styles.Dispose();
_colors.Dispose();
}
}

private static Vector4 Rgb(byte red, byte green, byte blue)
{
return new Vector4(red / 255f, green / 255f, blue / 255f, 1f);
}

private static Vector4 Rgba(byte red, byte green, byte blue, float alpha)
{
return new Vector4(red / 255f, green / 255f, blue / 255f, alpha);
}
}
Loading
Loading