1//////////////////////////////////////////////////////////////////////
2// This file is part of Remere's Map Editor
3//////////////////////////////////////////////////////////////////////
4// Remere's Map Editor is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Remere's Map Editor is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//////////////////////////////////////////////////////////////////////
17
18#include "main.h"
19
20#include "result_window.h"
21#include "gui.h"
22#include "position.h"
23#include "main_menubar.h"
24#include <vector>
25#include <algorithm>
26#include "string_utils.h"
27
28BEGIN_EVENT_TABLE(SearchResultWindow, wxPanel)
29EVT_LISTBOX(wxID_ANY, SearchResultWindow::OnClickResult)
30EVT_BUTTON(wxID_FILE, SearchResultWindow::OnClickExport)
31EVT_BUTTON(wxID_CLEAR, SearchResultWindow::OnClickClear)
32EVT_BUTTON(SEARCH_RESULT_NEXT_BUTTON, SearchResultWindow::OnClickNext)
33END_EVENT_TABLE()
34
35SearchResultWindow::SearchResultWindow(wxWindow* parent) :
36 wxPanel(parent, wxID_ANY),
37 use_ignored_ids(false),
38 last_search_itemid(0),
39 last_search_on_selection(false),
40 has_last_search(false),
41 last_ignored_ids_enabled(false) {
42 wxSizer* sizer = newd wxBoxSizer(wxVERTICAL);
43 result_list = newd wxListBox(this, wxID_ANY, wxDefaultPosition, wxSize(200, 330), 0, nullptr, wxLB_SINGLE | wxLB_ALWAYS_SB);
44 sizer->Add(result_list, wxSizerFlags(1).Expand());
45
46 wxSizer* buttonsSizer = newd wxBoxSizer(wxHORIZONTAL);
47 buttonsSizer->Add(newd wxButton(this, SEARCH_RESULT_NEXT_BUTTON, "Next"), wxSizerFlags(0).Center());
48 buttonsSizer->Add(newd wxButton(this, wxID_FILE, "Export"), wxSizerFlags(0).Center());
49 buttonsSizer->Add(newd wxButton(this, wxID_CLEAR, "Clear"), wxSizerFlags(0).Center());
50 sizer->Add(buttonsSizer, wxSizerFlags(0).Center().DoubleBorder());
51 SetSizerAndFit(sizer);
52}
53
54SearchResultWindow::~SearchResultWindow() {
55 Clear();
56}
57
58void SearchResultWindow::Clear() {
59 for (uint32_t n = 0; n < result_list->GetCount(); ++n) {
60 delete reinterpret_cast<Position*>(result_list->GetClientData(n));
61 }
62 result_list->Clear();
63 has_last_search = false;
64}
65
66void SearchResultWindow::AddPosition(wxString description, Position pos) {
67 uint16_t item_id = 0;
68 wxString item_name;
69
70 // Extract the item ID from the description
71 size_t start = description.Find("(ID: ");
72 if (start != wxString::npos) {
73 start += 4;
74 size_t end = description.find(')', start);
75 if (end != wxString::npos) {
76 wxString id_str = description.SubString(start, end - 1);
77 long temp_id;
78 if (id_str.ToLong(&temp_id)) {
79 item_id = static_cast<uint16_t>(temp_id);
80 item_name = description.SubString(0, start - 5).Trim(); // Get name without ID
81 OutputDebugStringA(wxString::Format("Parsed item: %s with ID: %d\n",
82 item_name, item_id).c_str());
83
84 if (use_ignored_ids) {
85 if (std::find(ignored_ids.begin(), ignored_ids.end(), item_id) != ignored_ids.end()) {
86 OutputDebugStringA(wxString::Format("Skipping ignored ID: %d\n", item_id).c_str());
87 return;
88 }
89
90 for (const auto& range : ignored_ranges) {
91 if (item_id >= range.first && item_id <= range.second) {
92 OutputDebugStringA(wxString::Format("Skipping ID in ignored range %d-%d: %d\n",
93 range.first, range.second, item_id).c_str());
94 return;
95 }
96 }
97 }
98 }
99 }
100 }
101
102 // If we couldn't parse an ID, try to get the item name directly
103 if (item_name.empty()) {
104 size_t space_pos = description.Find(' ');
105 if (space_pos != wxString::npos) {
106 item_name = description.SubString(0, space_pos - 1);
107 } else {
108 item_name = description;
109 }
110 OutputDebugStringA(wxString::Format("Using item name without ID: %s\n", item_name).c_str());
111 }
112
113 // Format the display text
114 wxString display_text;
115 if (item_id > 0) {
116 display_text = wxString::Format("%s [ID: %d] at (%d,%d,%d)",
117 item_name, item_id, pos.x, pos.y, pos.z);
118 } else {
119 display_text = wxString::Format("%s at (%d,%d,%d)",
120 item_name, pos.x, pos.y, pos.z);
121 }
122
123 result_list->Append(display_text, newd Position(pos));
124 OutputDebugStringA(wxString::Format("Added to result list: %s\n", display_text).c_str());
125}
126
127std::vector<Position> SearchResultWindow::GetFoundPositions() const {
128 std::vector<Position> positions;
129 for (uint32_t n = 0; n < result_list->GetCount(); ++n) {
130 Position* pos = reinterpret_cast<Position*>(result_list->GetClientData(n));
131 if (pos) {
132 positions.push_back(*pos);
133 }
134 }
135 return positions;
136}
137
138void SearchResultWindow::StoreSearchInfo(uint16_t itemId, bool onSelection) {
139 last_search_itemid = itemId;
140 last_search_on_selection = onSelection;
141 has_last_search = true;
142}
143
144void SearchResultWindow::OnClickResult(wxCommandEvent& event) {
145 Position* pos = reinterpret_cast<Position*>(event.GetClientData());
146 if (pos) {
147 g_gui.SetScreenCenterPosition(*pos);
148 }
149}
150
151void SearchResultWindow::OnClickExport(wxCommandEvent& WXUNUSED(event)) {
152 wxFileDialog dialog(this, "Save file...", "", "", "Text Documents (*.txt) | *.txt", wxFD_SAVE);
153 if (dialog.ShowModal() == wxID_OK) {
154 wxFile file(dialog.GetPath(), wxFile::write);
155 if (file.IsOpened()) {
156 g_gui.CreateLoadBar("Exporting search result...");
157
158 file.Write("Generated by Remere's Map Editor " + __RME_VERSION__);
159 file.Write("\n=============================================\n\n");
160 wxArrayString lines = result_list->GetStrings();
161 size_t count = lines.Count();
162 for (size_t i = 0; i < count; ++i) {
163 file.Write(lines[i] + "\n");
164 g_gui.SetLoadScale((int32_t)i, (int32_t)count);
165 }
166 file.Close();
167
168 g_gui.DestroyLoadBar();
169 }
170 }
171}
172
173void SearchResultWindow::OnClickClear(wxCommandEvent& WXUNUSED(event)) {
174 Clear();
175}
176
177void SearchResultWindow::OnClickNext(wxCommandEvent& WXUNUSED(event)) {
178 ContinueSearch();
179}
180
181void SearchResultWindow::ContinueSearch() {
182 if (!has_last_search) {
183 g_gui.PopupDialog("Search Error", "No previous search available to continue.", wxOK | wxICON_INFORMATION);
184 return;
185 }
186
187 // Store current positions to avoid duplicates
188 std::vector<Position> existingPositions = GetFoundPositions();
189
190 // Custom Finder that skips already found positions
191 class ContinuedFinder {
192 public:
193 ContinuedFinder(uint16_t itemId, const std::vector<Position>& existingPositions, uint32_t maxCount) :
194 itemId(itemId), existingPositions(existingPositions), maxCount(maxCount) {}
195
196 uint16_t itemId;
197 std::vector<Position> existingPositions;
198 uint32_t maxCount;
199 std::vector<std::pair<Tile*, Item*>> result;
200
201 bool limitReached() const {
202 return result.size() >= (size_t)maxCount;
203 }
204
205 bool isPositionAlreadyFound(const Position& pos) const {
206 for (const Position& existingPos : existingPositions) {
207 if (existingPos.x == pos.x && existingPos.y == pos.y && existingPos.z == pos.z) {
208 return true;
209 }
210 }
211 return false;
212 }
213
214 void operator()(Map& map, Tile* tile, Item* item, long long done) {
215 if (result.size() >= (size_t)maxCount) {
216 return;
217 }
218
219 if (done % 0x8000 == 0) {
220 g_gui.SetLoadDone((unsigned int)(100 * done / map.getTileCount()));
221 }
222
223 if (item->getID() == itemId) {
224 // Skip if this position was already found
225 if (!isPositionAlreadyFound(tile->getPosition())) {
226 result.push_back(std::make_pair(tile, item));
227 }
228 }
229 }
230 };
231
232 // Run the continued search
233 g_gui.CreateLoadBar("Continuing search...");
234
235 ContinuedFinder finder(last_search_itemid, existingPositions,
236 (uint32_t)g_settings.getInteger(Config::REPLACE_SIZE));
237
238 foreach_ItemOnMap(g_gui.GetCurrentMap(), finder, last_search_on_selection);
239 std::vector<std::pair<Tile*, Item*>>& result = finder.result;
240
241 g_gui.DestroyLoadBar();
242
243 if (finder.limitReached()) {
244 wxString msg;
245 msg << "The configured limit has been reached. Only " << finder.maxCount << " additional results will be displayed.";
246 g_gui.PopupDialog("Notice", msg, wxOK);
247 }
248
249 if (result.empty()) {
250 g_gui.PopupDialog("Search Complete", "No more matching items found.", wxOK | wxICON_INFORMATION);
251 return;
252 }
253
254 // Add new results to the window
255 for (const auto& pair : result) {
256 Tile* tile = pair.first;
257 Item* item = pair.second;
258
259 wxString description = wxString::Format("%s (ID: %d)",
260 wxstr(item->getName()),
261 item->getID());
262
263 AddPosition(description, tile->getPosition());
264 }
265}
266
267void SearchResultWindow::SetIgnoredIds(const wxString& ignored_ids_str, bool enable) {
268 use_ignored_ids = enable;
269 last_ignored_ids_text = ignored_ids_str;
270 last_ignored_ids_enabled = enable;
271
272 // Clear previous
273 ignored_ids.clear();
274 ignored_ranges.clear();
275
276 if (!ignored_ids_str.empty() && enable) {
277 // Parse the ignored IDs string
278 wxArrayString parts = wxStringTokenize(ignored_ids_str, ",");
279
280 for (const wxString& part : parts) {
281 // Trim whitespace - create a non-const copy to use Trim
282 wxString trimmed = part;
283 trimmed.Trim(true).Trim(false);
284
285 // Check if it's a range (contains "-")
286 size_t rangePos = trimmed.Find('-');
287 if (rangePos != wxString::npos) {
288 // It's a range
289 wxString firstStr = trimmed.Left(rangePos);
290 wxString secondStr = trimmed.Mid(rangePos + 1);
291
292 // Trim these strings as well
293 firstStr.Trim(true).Trim(false);
294 secondStr.Trim(true).Trim(false);
295
296 long first, second;
297 if (firstStr.ToLong(&first) && secondStr.ToLong(&second)) {
298 ignored_ranges.push_back(std::make_pair(
299 static_cast<uint16_t>(first),
300 static_cast<uint16_t>(second)));
301 }
302 }
303 else {
304 // It's a single ID
305 long id;
306 if (trimmed.ToLong(&id)) {
307 ignored_ids.push_back(static_cast<uint16_t>(id));
308 }
309 }
310 }
311 }
312}
313
314wxString SearchResultWindow::GetIgnoredItemsText() const {
315 return last_ignored_ids_text;
316}
317
318bool SearchResultWindow::IsIgnoreListEnabled() const {
319 return last_ignored_ids_enabled;
320}
321