yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
feature_flag_editor_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cstring>
6#include <filesystem>
7#include <fstream>
8#include <sstream>
9
10#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
13#include "core/project.h"
14#include "imgui/imgui.h"
15
16namespace yaze {
17namespace editor {
18
19namespace {
20
21// Status indicator colors
22constexpr ImVec4 kColorEnabled(0.2f, 0.8f, 0.2f, 1.0f); // Green
23constexpr ImVec4 kColorDisabled(0.8f, 0.2f, 0.2f, 1.0f); // Red
24constexpr ImVec4 kColorDirty(0.9f, 0.7f, 0.1f, 1.0f); // Yellow
25constexpr ImVec4 kColorInfo(0.6f, 0.6f, 0.6f, 1.0f); // Gray
26
27// Header comment block for the generated file
28constexpr const char* kFileHeader =
29 "; Feature override flags (for isolation testing).\n"
30 "; Set to 1 to enable a feature, 0 to disable it.\n"
31 "; This file is included after Util/macros.asm and overrides defaults.\n"
32 "; Generated by yaze Feature Flag Editor.\n\n";
33
34bool ContainsCaseInsensitive(const std::string& haystack,
35 const std::string& needle) {
36 if (needle.empty()) return true;
37 auto it = std::search(
38 haystack.begin(), haystack.end(), needle.begin(), needle.end(),
39 [](char a, char b) {
40 return std::tolower(static_cast<unsigned char>(a)) ==
41 std::tolower(static_cast<unsigned char>(b));
42 });
43 return it != haystack.end();
44}
45
46} // namespace
47
50
52 if (!project_) {
53 ImGui::TextDisabled("No project loaded.");
54 ImGui::TextWrapped(
55 "Open a yaze project with a hack_manifest.json to view feature flags.");
56 return;
57 }
58
59 const auto& manifest = project_->hack_manifest;
60 if (!manifest.loaded()) {
61 ImGui::TextDisabled("No hack manifest loaded.");
62 ImGui::TextWrapped(
63 "The project does not have a hack_manifest.json, or it failed to "
64 "load. Check the project settings to configure the manifest path.");
65 return;
66 }
67
68 // Refresh local copy if needed
69 if (needs_refresh_) {
71 needs_refresh_ = false;
72 }
73
74 // Header info
75 ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), ICON_MD_FLAG " %s",
76 manifest.hack_name().c_str());
77 ImGui::SameLine();
78 ImGui::TextDisabled("(%d flags)", static_cast<int>(flags_.size()));
79
80 // Config file path
81 std::string config_path = ResolveConfigPath();
82 if (!config_path.empty()) {
83 ImGui::TextDisabled("Config: %s", config_path.c_str());
84 }
85
86 ImGui::Separator();
87
88 // Toolbar: filter + actions
89 float save_width = ImGui::CalcTextSize(ICON_MD_SAVE " Save").x +
90 ImGui::GetStyle().FramePadding.x * 2.0f;
91 float refresh_width = ImGui::CalcTextSize(ICON_MD_REFRESH " Refresh").x +
92 ImGui::GetStyle().FramePadding.x * 2.0f;
93 float filter_width = ImGui::GetContentRegionAvail().x - save_width -
94 refresh_width - ImGui::GetStyle().ItemSpacing.x * 2.0f;
95
96 ImGui::SetNextItemWidth(std::max(100.0f, filter_width));
97 ImGui::InputTextWithHint("##flag_filter", "Filter flags...", filter_text_,
98 IM_ARRAYSIZE(filter_text_));
99 ImGui::SameLine();
100
101 // Count dirty flags
102 int dirty_count = 0;
103 for (const auto& flag : flags_) {
104 if (flag.dirty) dirty_count++;
105 }
106
107 {
108 std::optional<gui::StyleColorGuard> dirty_guard;
109 if (dirty_count > 0) {
110 dirty_guard.emplace(ImGuiCol_Button, kColorDirty);
111 }
112 if (ImGui::Button(
113 absl::StrFormat(ICON_MD_SAVE " Save%s",
114 dirty_count > 0
115 ? absl::StrFormat(" (%d)", dirty_count)
116 : "")
117 .c_str())) {
118 if (SaveToFile()) {
119 status_message_ = "Saved feature flags successfully.";
120 status_is_error_ = false;
121 // Mark all as clean
122 for (auto& flag : flags_) {
123 flag.dirty = false;
124 }
125 } else {
126 status_is_error_ = true;
127 // status_message_ already set by SaveToFile()
128 }
129 }
130 }
131
132 ImGui::SameLine();
133 if (ImGui::Button(ICON_MD_REFRESH " Refresh")) {
134 needs_refresh_ = true;
135 status_message_ = "Refreshed from manifest.";
136 status_is_error_ = false;
137 }
138
139 // Status message
140 if (!status_message_.empty()) {
141 ImVec4 color = status_is_error_ ? kColorDisabled : kColorEnabled;
142 ImGui::TextColored(color, "%s", status_message_.c_str());
143 }
144
145 ImGui::Spacing();
146
147 // Flags table
148 ImGuiTableFlags table_flags = ImGuiTableFlags_RowBg |
149 ImGuiTableFlags_BordersInnerH |
150 ImGuiTableFlags_SizingStretchProp |
151 ImGuiTableFlags_Resizable;
152
153 if (ImGui::BeginTable("FeatureFlagsTable", 5, table_flags)) {
154 ImGui::TableSetupColumn("Toggle", ImGuiTableColumnFlags_WidthFixed, 40.0f);
155 ImGui::TableSetupColumn("Flag Name", ImGuiTableColumnFlags_WidthStretch);
156 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 50.0f);
157 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 70.0f);
158 ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch,
159 0.4f);
160 ImGui::TableHeadersRow();
161
162 std::string filter(filter_text_);
163
164 for (int i = 0; i < static_cast<int>(flags_.size()); ++i) {
165 auto& flag = flags_[i];
166
167 // Apply filter
168 if (!filter.empty() &&
169 !ContainsCaseInsensitive(flag.name, filter) &&
170 !ContainsCaseInsensitive(flag.source, filter)) {
171 continue;
172 }
173
174 ImGui::PushID(i);
175 ImGui::TableNextRow();
176
177 // Toggle checkbox
178 ImGui::TableSetColumnIndex(0);
179 bool enabled = flag.enabled;
180 if (ImGui::Checkbox("##toggle", &enabled)) {
181 flag.enabled = enabled;
182 flag.value = enabled ? 1 : 0;
183 flag.dirty = true;
184 }
185
186 // Flag name
187 ImGui::TableSetColumnIndex(1);
188 if (flag.dirty) {
189 ImGui::TextColored(kColorDirty, "%s", flag.name.c_str());
190 } else {
191 ImGui::TextUnformatted(flag.name.c_str());
192 }
193
194 // Value
195 ImGui::TableSetColumnIndex(2);
196 ImGui::Text("%d", flag.value);
197
198 // Status badge
199 ImGui::TableSetColumnIndex(3);
200 if (flag.enabled) {
201 ImGui::TextColored(kColorEnabled, "ON");
202 } else {
203 ImGui::TextColored(kColorDisabled, "OFF");
204 }
205 if (flag.dirty) {
206 ImGui::SameLine();
207 ImGui::TextColored(kColorDirty, "*");
208 }
209
210 // Source
211 ImGui::TableSetColumnIndex(4);
212 ImGui::TextColored(kColorInfo, "%s", flag.source.c_str());
213
214 ImGui::PopID();
215 }
216
217 ImGui::EndTable();
218 }
219
220 // Summary
221 int enabled_count = 0;
222 for (const auto& flag : flags_) {
223 if (flag.enabled) enabled_count++;
224 }
225 ImGui::Separator();
226 ImGui::TextDisabled("%d/%d flags enabled", enabled_count,
227 static_cast<int>(flags_.size()));
228 if (dirty_count > 0) {
229 ImGui::SameLine();
230 ImGui::TextColored(kColorDirty, "(%d unsaved changes)", dirty_count);
231 }
232}
233
235 flags_.clear();
236
238 return;
239 }
240
241 const auto& manifest_flags = project_->hack_manifest.feature_flags();
242 flags_.reserve(manifest_flags.size());
243
244 for (const auto& mf : manifest_flags) {
245 EditableFlag ef;
246 ef.name = mf.name;
247 ef.value = mf.value;
248 ef.enabled = mf.enabled;
249 ef.source = mf.source;
250 ef.dirty = false;
251 flags_.push_back(std::move(ef));
252 }
253}
254
256 if (!project_) return "";
257
258 // Use code_folder from the project to find Config/feature_flags.asm
259 if (project_->code_folder.empty()) return "";
260
261 std::string code_path = project_->GetAbsolutePath(project_->code_folder);
262 auto candidate =
263 std::filesystem::path(code_path) / "Config" / "feature_flags.asm";
264 return candidate.string();
265}
266
268 std::string config_path = ResolveConfigPath();
269 if (config_path.empty()) {
270 status_message_ = "Cannot determine config file path (no code folder set).";
271 return false;
272 }
273
274 // Ensure the directory exists
275 auto parent = std::filesystem::path(config_path).parent_path();
276 if (!std::filesystem::exists(parent)) {
278 absl::StrFormat("Config directory does not exist: %s",
279 parent.string());
280 return false;
281 }
282
283 // Write to a temporary file, then rename for atomicity
284 std::string tmp_path = config_path + ".tmp";
285 {
286 std::ofstream out(tmp_path);
287 if (!out.is_open()) {
289 absl::StrFormat("Failed to open temp file: %s", tmp_path);
290 return false;
291 }
292
293 out << kFileHeader;
294
295 // Find the longest flag name for alignment
296 size_t max_name_len = 0;
297 for (const auto& flag : flags_) {
298 max_name_len = std::max(max_name_len, flag.name.size());
299 }
300
301 // Write each flag, aligned
302 for (const auto& flag : flags_) {
303 // Pad the flag name for visual alignment
304 std::string padded_name = flag.name;
305 while (padded_name.size() < max_name_len) {
306 padded_name += ' ';
307 }
308 out << padded_name << " = " << flag.value << "\n";
309 }
310 }
311
312 // Atomic rename
313 std::error_code ec;
314 std::filesystem::rename(tmp_path, config_path, ec);
315 if (ec) {
317 absl::StrFormat("Failed to rename temp file: %s", ec.message());
318 // Clean up temp file
319 std::filesystem::remove(tmp_path, ec);
320 return false;
321 }
322
323 return true;
324}
325
326} // namespace editor
327} // namespace yaze
const std::vector< FeatureFlag > & feature_flags() const
bool loaded() const
Check if the manifest has been loaded.
void RefreshFromManifest()
Refresh the local flags list from the hack manifest.
bool SaveToFile()
Write the current flag state to Config/feature_flags.asm.
void Draw()
Draw the panel content (no ImGui::Begin/End).
std::string ResolveConfigPath() const
Resolve the absolute path to Config/feature_flags.asm.
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_FLAG
Definition icons.h:784
#define ICON_MD_SAVE
Definition icons.h:1644
constexpr ImVec4 kColorDisabled(0.8f, 0.2f, 0.2f, 1.0f)
constexpr ImVec4 kColorEnabled(0.2f, 0.8f, 0.2f, 1.0f)
bool ContainsCaseInsensitive(const std::string &haystack, const std::string &needle)
constexpr ImVec4 kColorDirty(0.9f, 0.7f, 0.1f, 1.0f)
constexpr ImVec4 kColorInfo(0.6f, 0.6f, 0.6f, 1.0f)
core::HackManifest hack_manifest
Definition project.h:160
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1287