yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_issue_report_storage.cc
Go to the documentation of this file.
2
3#include <cctype>
4#include <cstdio>
5#include <fstream>
6#include <string>
7
8#include "absl/status/status.h"
9#include "absl/strings/str_format.h"
10#include "util/platform_paths.h"
11
12namespace yaze::editor {
13namespace {
14
15constexpr char kIssueReportsSubdir[] = "issue_reports";
16constexpr char kDungeonReportsSubdir[] = "dungeon";
17constexpr char kDungeonScreenshotsSubdir[] = "screenshots";
18constexpr char kDungeonIssueLogFilename[] = "dungeon_issues.md";
19constexpr char kDefaultIssueHeading[] = "Dungeon issue report";
20constexpr char kUnknownTimestampLabel[] = "Unknown time";
21constexpr char kObservedIssueHeading[] = "### Observed Issue\n";
22constexpr char kDiagnosticsHeading[] = "### Diagnostics\n";
23
24void WriteDetailLine(std::ofstream& out, const char* label,
25 const std::string& value) {
26 if (!value.empty()) {
27 out << "- " << label << ": " << value << "\n";
28 }
29}
30
31void WriteRoomLine(std::ofstream& out, int room_id) {
32 if (room_id >= 0) {
33 out << "- Room: 0x" << absl::StrFormat("%03X", room_id) << "\n";
34 }
35}
36
37std::string SanitizeFilenameComponent(std::string value) {
38 for (char& ch : value) {
39 const unsigned char uch = static_cast<unsigned char>(ch);
40 if ((uch >= 'a' && uch <= 'z') || (uch >= 'A' && uch <= 'Z') ||
41 (uch >= '0' && uch <= '9')) {
42 ch = static_cast<char>(std::tolower(uch));
43 } else {
44 ch = '_';
45 }
46 }
47
48 std::string sanitized;
49 sanitized.reserve(value.size());
50 bool last_was_underscore = false;
51 for (char ch : value) {
52 if (ch == '_') {
53 if (!last_was_underscore) {
54 sanitized.push_back(ch);
55 }
56 last_was_underscore = true;
57 continue;
58 }
59 sanitized.push_back(ch);
60 last_was_underscore = false;
61 }
62
63 while (!sanitized.empty() && sanitized.front() == '_') {
64 sanitized.erase(sanitized.begin());
65 }
66 while (!sanitized.empty() && sanitized.back() == '_') {
67 sanitized.pop_back();
68 }
69
70 if (sanitized.empty()) {
71 sanitized = "issue";
72 }
73 return sanitized;
74}
75
76std::string BuildRoomFileStem(int room_id) {
77 return room_id >= 0 ? absl::StrFormat("room_%03x", room_id) : "room_unknown";
78}
79
80} // namespace
81
82absl::StatusOr<DungeonIssueReportPaths> ResolveDungeonIssueReportPaths() {
83 auto base_dir_or =
85 if (!base_dir_or.ok()) {
86 return base_dir_or.status();
87 }
88
90 paths.reports_dir = *base_dir_or / kDungeonReportsSubdir;
92 if (!status.ok()) {
93 return status;
94 }
95
96 paths.screenshots_dir = paths.reports_dir / kDungeonScreenshotsSubdir;
98 if (!status.ok()) {
99 return status;
100 }
101
102 paths.log_path = paths.reports_dir / kDungeonIssueLogFilename;
103 return paths;
104}
105
106absl::StatusOr<std::filesystem::path> BuildDungeonIssueScreenshotPath(
107 int room_id, const std::string& category_label,
108 const std::string& timestamp_slug) {
109 auto paths_or = ResolveDungeonIssueReportPaths();
110 if (!paths_or.ok()) {
111 return paths_or.status();
112 }
113
114 return paths_or->screenshots_dir /
115 absl::StrFormat("%s_%s_%s.bmp", BuildRoomFileStem(room_id),
116 SanitizeFilenameComponent(category_label),
117 timestamp_slug);
118}
119
120absl::StatusOr<std::filesystem::path> AppendDungeonIssueLogEntry(
121 const DungeonIssueLogEntry& entry) {
122 auto paths_or = ResolveDungeonIssueReportPaths();
123 if (!paths_or.ok()) {
124 return paths_or.status();
125 }
126
127 const std::filesystem::path& log_path = paths_or->log_path;
128 std::ofstream out(log_path, std::ios::app);
129 if (!out) {
130 return absl::InternalError(
131 absl::StrFormat("Failed to open issue log: %s", log_path.string()));
132 }
133
134 const std::string heading =
135 entry.heading.empty() ? std::string(kDefaultIssueHeading) : entry.heading;
136 const std::string timestamp = entry.timestamp_display.empty()
137 ? std::string(kUnknownTimestampLabel)
138 : entry.timestamp_display;
139
140 out << "## " << timestamp << " - " << heading << "\n";
141 WriteDetailLine(out, "Category", entry.category_label);
142 WriteDetailLine(out, "Scope", entry.scope);
143 WriteRoomLine(out, entry.room_id);
144 WriteDetailLine(out, "Screenshot", entry.screenshot_path);
145 out << "\n";
146
147 if (!entry.observed_issue.empty()) {
148 out << kObservedIssueHeading;
149 out << entry.observed_issue << "\n\n";
150 }
151
152 out << kDiagnosticsHeading;
153 out << "```text\n" << entry.diagnostics << "\n```\n\n";
154 out.flush();
155 if (!out) {
156 return absl::InternalError(absl::StrFormat(
157 "Failed while writing issue log: %s", log_path.string()));
158 }
159
160 return log_path;
161}
162
163} // namespace yaze::editor
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
void WriteDetailLine(std::ofstream &out, const char *label, const std::string &value)
Editors are the view controllers for the application.
absl::StatusOr< std::filesystem::path > AppendDungeonIssueLogEntry(const DungeonIssueLogEntry &entry)
absl::StatusOr< std::filesystem::path > BuildDungeonIssueScreenshotPath(int room_id, const std::string &category_label, const std::string &timestamp_slug)
absl::StatusOr< DungeonIssueReportPaths > ResolveDungeonIssueReportPaths()