| @@ -0,0 +1,174 @@ | |||
| 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 | + | #ifndef RME_MAP_REGION_H | |
| 19 | + | #define RME_MAP_REGION_H | |
| 20 | + | ||
| 21 | + | #include "position.h" | |
| 22 | + | ||
| 23 | + | class Tile; | |
| 24 | + | class Floor; | |
| 25 | + | class BaseMap; | |
| 26 | + | ||
| 27 | + | class TileLocation { | |
| 28 | + | TileLocation(); | |
| 29 | + | ||
| 30 | + | public: | |
| 31 | + | ~TileLocation(); | |
| 32 | + | ||
| 33 | + | TileLocation(const TileLocation&) = delete; | |
| 34 | + | TileLocation& operator=(const TileLocation&) = delete; | |
| 35 | + | ||
| 36 | + | protected: | |
| 37 | + | Tile* tile; | |
| 38 | + | Position position; | |
| 39 | + | size_t spawn_count; | |
| 40 | + | size_t waypoint_count; | |
| 41 | + | size_t town_count; | |
| 42 | + | HouseExitList* house_exits; // Any house exits pointing here | |
| 43 | + | ||
| 44 | + | public: | |
| 45 | + | // Access tile | |
| 46 | + | // Can't set directly since that does not update tile count | |
| 47 | + | Tile* get() { | |
| 48 | + | return tile; | |
| 49 | + | } | |
| 50 | + | const Tile* get() const { | |
| 51 | + | return tile; | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | int size() const; | |
| 55 | + | bool empty() const; | |
| 56 | + | ||
| 57 | + | Position getPosition() const { | |
| 58 | + | return position; | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | int getX() const { | |
| 62 | + | return position.x; | |
| 63 | + | } | |
| 64 | + | int getY() const { | |
| 65 | + | return position.y; | |
| 66 | + | } | |
| 67 | + | int getZ() const { | |
| 68 | + | return position.z; | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | size_t getSpawnCount() const { | |
| 72 | + | return spawn_count; | |
| 73 | + | } | |
| 74 | + | void increaseSpawnCount() { | |
| 75 | + | spawn_count++; | |
| 76 | + | } | |
| 77 | + | void decreaseSpawnCount() { | |
| 78 | + | spawn_count--; | |
| 79 | + | } | |
| 80 | + | size_t getWaypointCount() const { | |
| 81 | + | return waypoint_count; | |
| 82 | + | } | |
| 83 | + | void increaseWaypointCount() { | |
| 84 | + | waypoint_count++; | |
| 85 | + | } | |
| 86 | + | void decreaseWaypointCount() { | |
| 87 | + | waypoint_count--; | |
| 88 | + | } | |
| 89 | + | size_t getTownCount() const { | |
| 90 | + | return town_count; | |
| 91 | + | } | |
| 92 | + | void increaseTownCount() { | |
| 93 | + | town_count++; | |
| 94 | + | } | |
| 95 | + | void decreaseTownCount() { | |
| 96 | + | town_count--; | |
| 97 | + | } | |
| 98 | + | HouseExitList* createHouseExits() { | |
| 99 | + | if (house_exits) { | |
| 100 | + | return house_exits; | |
| 101 | + | } | |
| 102 | + | return house_exits = newd HouseExitList; | |
| 103 | + | } | |
| 104 | + | HouseExitList* getHouseExits() { | |
| 105 | + | return house_exits; | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | friend class Floor; | |
| 109 | + | friend class QTreeNode; | |
| 110 | + | friend class Waypoints; | |
| 111 | + | }; | |
| 112 | + | ||
| 113 | + | class Floor { | |
| 114 | + | public: | |
| 115 | + | Floor(int x, int y, int z); | |
| 116 | + | TileLocation locs[MAP_LAYERS]; | |
| 117 | + | }; | |
| 118 | + | ||
| 119 | + | // This is not a QuadTree, but a HexTree (16 child nodes to every node), so the name is abit misleading | |
| 120 | + | class QTreeNode { | |
| 121 | + | public: | |
| 122 | + | QTreeNode(BaseMap& map); | |
| 123 | + | virtual ~QTreeNode(); | |
| 124 | + | ||
| 125 | + | QTreeNode(const QTreeNode&) = delete; | |
| 126 | + | QTreeNode& operator=(const QTreeNode&) = delete; | |
| 127 | + | ||
| 128 | + | QTreeNode* getLeaf(int x, int y); // Might return nullptr | |
| 129 | + | QTreeNode* getLeafForce(int x, int y); // Will never return nullptr, it will create the node if it's not there | |
| 130 | + | ||
| 131 | + | // Coordinates are NOT relative | |
| 132 | + | TileLocation* createTile(int x, int y, int z); | |
| 133 | + | TileLocation* getTile(int x, int y, int z); | |
| 134 | + | Tile* setTile(int x, int y, int z, Tile* tile); | |
| 135 | + | void clearTile(int x, int y, int z); | |
| 136 | + | ||
| 137 | + | Floor* createFloor(int x, int y, int z); | |
| 138 | + | Floor* getFloor(uint32_t z) { | |
| 139 | + | ASSERT(isLeaf); | |
| 140 | + | return array[z]; | |
| 141 | + | } | |
| 142 | + | Floor** getFloors() { | |
| 143 | + | return array; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | void setVisible(bool overground, bool underground); | |
| 147 | + | void setVisible(uint32_t client, bool underground, bool value); | |
| 148 | + | bool isVisible(uint32_t client, bool underground); | |
| 149 | + | void clearVisible(uint32_t client); | |
| 150 | + | ||
| 151 | + | void setRequested(bool underground, bool r); | |
| 152 | + | bool isVisible(bool underground); | |
| 153 | + | bool isRequested(bool underground); | |
| 154 | + | ||
| 155 | + | protected: | |
| 156 | + | BaseMap& map; | |
| 157 | + | uint32_t visible; | |
| 158 | + | ||
| 159 | + | bool isLeaf; | |
| 160 | + | union { | |
| 161 | + | QTreeNode* child[MAP_LAYERS]; | |
| 162 | + | Floor* array[MAP_LAYERS]; | |
| 163 | + | /* | |
| 164 | + | #if 16 != MAP_LAYERS | |
| 165 | + | # error "You need to rewrite the QuadTree in order to handle more or less than 16 floors" | |
| 166 | + | #endif | |
| 167 | + | */ | |
| 168 | + | }; | |
| 169 | + | ||
| 170 | + | friend class BaseMap; | |
| 171 | + | friend class MapIterator; | |
| 172 | + | }; | |
| 173 | + | ||
| 174 | + | #endif |
| @@ -0,0 +1,300 @@ | |||
| 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 | + | #include "map_summary_window.h" | |
| 20 | + | #include "gui.h" | |
| 21 | + | #include "map.h" | |
| 22 | + | #include "tile.h" | |
| 23 | + | #include "item.h" | |
| 24 | + | #include "items.h" | |
| 25 | + | #include "string_utils.h" | |
| 26 | + | #include <algorithm> | |
| 27 | + | #include <map> | |
| 28 | + | ||
| 29 | + | BEGIN_EVENT_TABLE(MapSummaryWindow, wxPanel) | |
| 30 | + | EVT_LISTBOX(wxID_ANY, MapSummaryWindow::OnClickResult) | |
| 31 | + | EVT_BUTTON(wxID_FILE, MapSummaryWindow::OnClickExport) | |
| 32 | + | EVT_BUTTON(wxID_CLEAR, MapSummaryWindow::OnClickClear) | |
| 33 | + | EVT_BUTTON(wxID_REFRESH, MapSummaryWindow::OnClickSummarize) | |
| 34 | + | EVT_TEXT(wxID_FIND, MapSummaryWindow::OnFilterText) | |
| 35 | + | EVT_BUTTON(wxID_SORT_ASCENDING, MapSummaryWindow::OnClickSort) | |
| 36 | + | END_EVENT_TABLE() | |
| 37 | + | ||
| 38 | + | MapSummaryWindow::MapSummaryWindow(wxWindow* parent) : | |
| 39 | + | wxPanel(parent, wxID_ANY) { | |
| 40 | + | ||
| 41 | + | wxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); | |
| 42 | + | ||
| 43 | + | // Filter section | |
| 44 | + | wxSizer* filter_sizer = new wxBoxSizer(wxHORIZONTAL); | |
| 45 | + | filter_sizer->Add(new wxStaticText(this, wxID_ANY, "Filter:"), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); | |
| 46 | + | filter_text = new wxTextCtrl(this, wxID_FIND, "", wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); | |
| 47 | + | filter_sizer->Add(filter_text, 1, wxEXPAND | wxALL, 5); | |
| 48 | + | main_sizer->Add(filter_sizer, 0, wxEXPAND | wxALL, 5); | |
| 49 | + | ||
| 50 | + | // List | |
| 51 | + | result_list = new wxListBox(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400), 0, nullptr, wxLB_SINGLE | wxLB_ALWAYS_SB); | |
| 52 | + | main_sizer->Add(result_list, 1, wxEXPAND | wxALL, 5); | |
| 53 | + | ||
| 54 | + | // Buttons | |
| 55 | + | wxSizer* button_sizer = new wxBoxSizer(wxHORIZONTAL); | |
| 56 | + | ||
| 57 | + | summarize_button = new wxButton(this, wxID_REFRESH, "Summarize Map"); | |
| 58 | + | button_sizer->Add(summarize_button, 0, wxALL, 5); | |
| 59 | + | ||
| 60 | + | // Sort buttons | |
| 61 | + | sort_count_button = new wxButton(this, wxID_SORT_ASCENDING, "Sort by Count"); | |
| 62 | + | sort_id_button = new wxButton(this, wxID_SORT_ASCENDING + 1, "Sort by ID"); | |
| 63 | + | sort_name_button = new wxButton(this, wxID_SORT_ASCENDING + 2, "Sort by Name"); | |
| 64 | + | ||
| 65 | + | button_sizer->Add(sort_count_button, 0, wxALL, 5); | |
| 66 | + | button_sizer->Add(sort_id_button, 0, wxALL, 5); | |
| 67 | + | button_sizer->Add(sort_name_button, 0, wxALL, 5); | |
| 68 | + | ||
| 69 | + | button_sizer->Add(new wxButton(this, wxID_FILE, "Export"), 0, wxALL, 5); | |
| 70 | + | button_sizer->Add(new wxButton(this, wxID_CLEAR, "Clear"), 0, wxALL, 5); | |
| 71 | + | ||
| 72 | + | main_sizer->Add(button_sizer, 0, wxCENTER | wxALL, 5); | |
| 73 | + | ||
| 74 | + | SetSizerAndFit(main_sizer); | |
| 75 | + | ||
| 76 | + | // Bind sort button events | |
| 77 | + | Bind(wxEVT_BUTTON, &MapSummaryWindow::OnClickSort, this, wxID_SORT_ASCENDING); | |
| 78 | + | Bind(wxEVT_BUTTON, &MapSummaryWindow::OnClickSort, this, wxID_SORT_ASCENDING + 1); | |
| 79 | + | Bind(wxEVT_BUTTON, &MapSummaryWindow::OnClickSort, this, wxID_SORT_ASCENDING + 2); | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | MapSummaryWindow::~MapSummaryWindow() { | |
| 83 | + | Clear(); | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | void MapSummaryWindow::Clear() { | |
| 87 | + | result_list->Clear(); | |
| 88 | + | item_summaries.clear(); | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | void MapSummaryWindow::SummarizeMap(Map& map) { | |
| 92 | + | Clear(); | |
| 93 | + | ||
| 94 | + | // Count all items on the map | |
| 95 | + | std::map<uint16_t, uint32_t> item_counts; | |
| 96 | + | ||
| 97 | + | g_gui.CreateLoadBar("Summarizing map items..."); | |
| 98 | + | ||
| 99 | + | uint64_t total_tiles = map.getTileCount(); | |
| 100 | + | uint64_t processed = 0; | |
| 101 | + | ||
| 102 | + | for (MapIterator mit = map.begin(); mit != map.end(); ++mit) { | |
| 103 | + | Tile* tile = (*mit)->get(); | |
| 104 | + | if (!tile) { | |
| 105 | + | processed++; | |
| 106 | + | continue; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // Update progress bar every 1000 tiles | |
| 110 | + | if (processed % 1000 == 0) { | |
| 111 | + | g_gui.SetLoadDone((uint32_t)(100 * processed / total_tiles)); | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Count ground items | |
| 115 | + | if (tile->ground) { | |
| 116 | + | item_counts[tile->ground->getID()]++; | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // Count items on tile | |
| 120 | + | for (Item* item : tile->items) { | |
| 121 | + | if (item) { | |
| 122 | + | item_counts[item->getID()]++; | |
| 123 | + | ||
| 124 | + | // Count items in containers recursively | |
| 125 | + | if (Container* container = dynamic_cast<Container*>(item)) { | |
| 126 | + | CountContainerItems(container, item_counts); | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | processed++; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | g_gui.DestroyLoadBar(); | |
| 135 | + | ||
| 136 | + | // Convert to our internal format and add to list | |
| 137 | + | for (const auto& pair : item_counts) { | |
| 138 | + | uint16_t itemId = pair.first; | |
| 139 | + | uint32_t count = pair.second; | |
| 140 | + | ||
| 141 | + | ItemType& itemType = g_items[itemId]; | |
| 142 | + | wxString itemName = wxstr(itemType.name); | |
| 143 | + | if (itemName.empty()) { | |
| 144 | + | itemName = wxString::Format("Unknown Item %d", itemId); | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | item_summaries.emplace_back(itemId, itemName, count); | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // Sort by count (descending) by default | |
| 151 | + | SortByCount(); | |
| 152 | + | RefreshList(); | |
| 153 | + | ||
| 154 | + | wxString statusMsg = wxString::Format("Map summarized: %zu unique item types found", item_summaries.size()); | |
| 155 | + | g_gui.SetStatusText(statusMsg); | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | void MapSummaryWindow::CountContainerItems(Container* container, std::map<uint16_t, uint32_t>& item_counts) { | |
| 159 | + | const ItemVector& items = container->getVector(); | |
| 160 | + | for (Item* item : items) { | |
| 161 | + | if (item) { | |
| 162 | + | item_counts[item->getID()]++; | |
| 163 | + | ||
| 164 | + | // Recursively count items in nested containers | |
| 165 | + | if (Container* nested_container = dynamic_cast<Container*>(item)) { | |
| 166 | + | CountContainerItems(nested_container, item_counts); | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | void MapSummaryWindow::AddItemCount(uint16_t itemId, const wxString& itemName, uint32_t count) { | |
| 173 | + | item_summaries.emplace_back(itemId, itemName, count); | |
| 174 | + | RefreshList(); | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | void MapSummaryWindow::SetFilter(const wxString& filter) { | |
| 178 | + | current_filter = filter.Lower(); | |
| 179 | + | RefreshList(); | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | void MapSummaryWindow::RefreshList() { | |
| 183 | + | result_list->Clear(); | |
| 184 | + | ||
| 185 | + | for (const auto& summary : item_summaries) { | |
| 186 | + | // Apply filter if set | |
| 187 | + | if (!current_filter.empty()) { | |
| 188 | + | wxString itemNameLower = summary.itemName.Lower(); | |
| 189 | + | wxString itemIdStr = wxString::Format("%d", summary.itemId); | |
| 190 | + | ||
| 191 | + | if (itemNameLower.Find(current_filter) == wxNOT_FOUND && | |
| 192 | + | itemIdStr.Find(current_filter) == wxNOT_FOUND) { | |
| 193 | + | continue; | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | wxString display_text = wxString::Format("%s [ID: %d] - Count: %u", | |
| 198 | + | summary.itemName, summary.itemId, summary.count); | |
| 199 | + | result_list->Append(display_text); | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | void MapSummaryWindow::SortByCount() { | |
| 204 | + | std::sort(item_summaries.begin(), item_summaries.end(), | |
| 205 | + | [](const ItemSummary& a, const ItemSummary& b) { | |
| 206 | + | return a.count > b.count; // Descending order | |
| 207 | + | }); | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | void MapSummaryWindow::SortByID() { | |
| 211 | + | std::sort(item_summaries.begin(), item_summaries.end(), | |
| 212 | + | [](const ItemSummary& a, const ItemSummary& b) { | |
| 213 | + | return a.itemId < b.itemId; // Ascending order | |
| 214 | + | }); | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | void MapSummaryWindow::SortByName() { | |
| 218 | + | std::sort(item_summaries.begin(), item_summaries.end(), | |
| 219 | + | [](const ItemSummary& a, const ItemSummary& b) { | |
| 220 | + | return a.itemName.CmpNoCase(b.itemName) < 0; // Case-insensitive ascending | |
| 221 | + | }); | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | void MapSummaryWindow::OnClickResult(wxCommandEvent& event) { | |
| 225 | + | // Could implement going to first occurrence of selected item | |
| 226 | + | int selection = result_list->GetSelection(); | |
| 227 | + | if (selection != wxNOT_FOUND) { | |
| 228 | + | // For now, just show a message with the selected item info | |
| 229 | + | wxString selected_text = result_list->GetString(selection); | |
| 230 | + | g_gui.SetStatusText("Selected: " + selected_text); | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | void MapSummaryWindow::OnClickExport(wxCommandEvent& WXUNUSED(event)) { | |
| 235 | + | wxFileDialog dialog(this, "Save map summary...", "", "", "Text Documents (*.txt) | *.txt", wxFD_SAVE); | |
| 236 | + | if (dialog.ShowModal() == wxID_OK) { | |
| 237 | + | wxFile file(dialog.GetPath(), wxFile::write); | |
| 238 | + | if (file.IsOpened()) { | |
| 239 | + | file.Write("Map Item Summary\n"); | |
| 240 | + | file.Write("Generated by Remere's Map Editor " + __RME_VERSION__ + "\n"); | |
| 241 | + | file.Write("======================================\n\n"); | |
| 242 | + | ||
| 243 | + | for (const auto& summary : item_summaries) { | |
| 244 | + | // Apply filter if set | |
| 245 | + | if (!current_filter.empty()) { | |
| 246 | + | wxString itemNameLower = summary.itemName.Lower(); | |
| 247 | + | wxString itemIdStr = wxString::Format("%d", summary.itemId); | |
| 248 | + | ||
| 249 | + | if (itemNameLower.Find(current_filter) == wxNOT_FOUND && | |
| 250 | + | itemIdStr.Find(current_filter) == wxNOT_FOUND) { | |
| 251 | + | continue; | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | wxString line = wxString::Format("%s [ID: %d] - Count: %u\n", | |
| 256 | + | summary.itemName, summary.itemId, summary.count); | |
| 257 | + | file.Write(line); | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | file.Close(); | |
| 261 | + | g_gui.SetStatusText("Map summary exported successfully"); | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | void MapSummaryWindow::OnClickClear(wxCommandEvent& WXUNUSED(event)) { | |
| 267 | + | Clear(); | |
| 268 | + | g_gui.SetStatusText("Map summary cleared"); | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | void MapSummaryWindow::OnClickSummarize(wxCommandEvent& WXUNUSED(event)) { | |
| 272 | + | if (!g_gui.IsEditorOpen()) { | |
| 273 | + | g_gui.PopupDialog("Error", "No map is currently open!", wxOK | wxICON_ERROR); | |
| 274 | + | return; | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | Map& current_map = g_gui.GetCurrentMap(); | |
| 278 | + | SummarizeMap(current_map); | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | void MapSummaryWindow::OnFilterText(wxCommandEvent& WXUNUSED(event)) { | |
| 282 | + | SetFilter(filter_text->GetValue()); | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | void MapSummaryWindow::OnClickSort(wxCommandEvent& event) { | |
| 286 | + | int id = event.GetId(); | |
| 287 | + | ||
| 288 | + | if (id == wxID_SORT_ASCENDING) { | |
| 289 | + | // Sort by count | |
| 290 | + | SortByCount(); | |
| 291 | + | } else if (id == wxID_SORT_ASCENDING + 1) { | |
| 292 | + | // Sort by ID | |
| 293 | + | SortByID(); | |
| 294 | + | } else if (id == wxID_SORT_ASCENDING + 2) { | |
| 295 | + | // Sort by name | |
| 296 | + | SortByName(); | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | RefreshList(); | |
| 300 | + | } |
| @@ -0,0 +1,76 @@ | |||
| 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 | + | #ifndef RME_MAP_SUMMARY_WINDOW_H_ | |
| 19 | + | #define RME_MAP_SUMMARY_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "main.h" | |
| 22 | + | #include <map> | |
| 23 | + | #include <vector> | |
| 24 | + | ||
| 25 | + | // Forward declarations | |
| 26 | + | class Map; | |
| 27 | + | class Container; | |
| 28 | + | ||
| 29 | + | class MapSummaryWindow : public wxPanel { | |
| 30 | + | public: | |
| 31 | + | MapSummaryWindow(wxWindow* parent); | |
| 32 | + | virtual ~MapSummaryWindow(); | |
| 33 | + | ||
| 34 | + | void Clear(); | |
| 35 | + | void SummarizeMap(Map& map); | |
| 36 | + | void AddItemCount(uint16_t itemId, const wxString& itemName, uint32_t count); | |
| 37 | + | void SetFilter(const wxString& filter); | |
| 38 | + | ||
| 39 | + | // Event handlers | |
| 40 | + | void OnClickResult(wxCommandEvent& event); | |
| 41 | + | void OnClickExport(wxCommandEvent& event); | |
| 42 | + | void OnClickClear(wxCommandEvent& event); | |
| 43 | + | void OnClickSummarize(wxCommandEvent& event); | |
| 44 | + | void OnFilterText(wxCommandEvent& event); | |
| 45 | + | ||
| 46 | + | protected: | |
| 47 | + | struct ItemSummary { | |
| 48 | + | uint16_t itemId; | |
| 49 | + | wxString itemName; | |
| 50 | + | uint32_t count; | |
| 51 | + | ||
| 52 | + | ItemSummary(uint16_t id, const wxString& name, uint32_t c) | |
| 53 | + | : itemId(id), itemName(name), count(c) {} | |
| 54 | + | }; | |
| 55 | + | ||
| 56 | + | void RefreshList(); | |
| 57 | + | void SortByCount(); | |
| 58 | + | void SortByID(); | |
| 59 | + | void SortByName(); | |
| 60 | + | void CountContainerItems(Container* container, std::map<uint16_t, uint32_t>& item_counts); | |
| 61 | + | void OnClickSort(wxCommandEvent& event); | |
| 62 | + | ||
| 63 | + | wxListBox* result_list; | |
| 64 | + | wxTextCtrl* filter_text; | |
| 65 | + | wxButton* summarize_button; | |
| 66 | + | wxButton* sort_count_button; | |
| 67 | + | wxButton* sort_id_button; | |
| 68 | + | wxButton* sort_name_button; | |
| 69 | + | ||
| 70 | + | std::vector<ItemSummary> item_summaries; | |
| 71 | + | wxString current_filter; | |
| 72 | + | ||
| 73 | + | DECLARE_EVENT_TABLE() | |
| 74 | + | }; | |
| 75 | + | ||
| 76 | + | #endif // RME_MAP_SUMMARY_WINDOW_H_ |
| @@ -0,0 +1,109 @@ | |||
| 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 "gui.h" | |
| 21 | + | #include "editor.h" | |
| 22 | + | #include "map.h" | |
| 23 | + | #include "sprites.h" | |
| 24 | + | #include "map_tab.h" | |
| 25 | + | #include "editor_tabs.h" | |
| 26 | + | #include "map_display.h" | |
| 27 | + | ||
| 28 | + | MapTab::MapTab(MapTabbook* aui, Editor* editor) : | |
| 29 | + | EditorTab(), | |
| 30 | + | MapWindow(aui, *editor), | |
| 31 | + | aui(aui) { | |
| 32 | + | iref = newd InternalReference; | |
| 33 | + | iref->editor = editor; | |
| 34 | + | iref->owner_count = 1; | |
| 35 | + | ||
| 36 | + | aui->AddTab(this, true); | |
| 37 | + | FitToMap(); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | MapTab::MapTab(const MapTab* other) : | |
| 41 | + | EditorTab(), | |
| 42 | + | MapWindow(other->aui, *other->iref->editor), | |
| 43 | + | aui(other->aui), | |
| 44 | + | iref(other->iref) { | |
| 45 | + | iref->owner_count++; | |
| 46 | + | aui->AddTab(this, true); | |
| 47 | + | FitToMap(); | |
| 48 | + | int x, y; | |
| 49 | + | other->GetCanvas()->GetScreenCenter(&x, &y); | |
| 50 | + | SetScreenCenterPosition(Position(x, y, other->GetCanvas()->GetFloor())); | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | MapTab::~MapTab() { | |
| 54 | + | iref->owner_count--; | |
| 55 | + | if (iref->owner_count <= 0) { | |
| 56 | + | delete iref->editor; | |
| 57 | + | delete iref; | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | bool MapTab::IsUniqueReference() const { | |
| 62 | + | return iref->owner_count == 1; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | wxWindow* MapTab::GetWindow() const { | |
| 66 | + | return const_cast<MapTab*>(this); | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | MapCanvas* MapTab::GetCanvas() const { | |
| 70 | + | return canvas; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | MapWindow* MapTab::GetView() const { | |
| 74 | + | return const_cast<MapWindow*>((const MapWindow*)this); | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | wxString MapTab::GetTitle() const { | |
| 78 | + | wxString ss; | |
| 79 | + | ss << wxstr(iref->editor->map.getName()) << (iref->editor->map.hasChanged() ? "*" : ""); | |
| 80 | + | return ss; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | Editor* MapTab::GetEditor() const { | |
| 84 | + | return &editor; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | Map* MapTab::GetMap() const { | |
| 88 | + | return &editor.map; | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | Position MapTab::GetScreenCenterPosition() const { | |
| 92 | + | // Forward to the MapWindow implementation | |
| 93 | + | return MapWindow::GetScreenCenterPosition(); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | void MapTab::VisibilityCheck() { | |
| 97 | + | EditorTab* editorTab = aui->GetCurrentTab(); | |
| 98 | + | MapTab* mapTab = dynamic_cast<MapTab*>(editorTab); | |
| 99 | + | UpdateDialogs(mapTab && HasSameReference(mapTab)); | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | void MapTab::OnSwitchEditorMode(EditorMode mode) { | |
| 103 | + | gem->SetSprite(mode == DRAWING_MODE ? EDITOR_SPRITE_DRAWING_GEM : EDITOR_SPRITE_SELECTION_GEM); | |
| 104 | + | if (mode == SELECTION_MODE) { | |
| 105 | + | canvas->EnterSelectionMode(); | |
| 106 | + | } else { | |
| 107 | + | canvas->EnterDrawingMode(); | |
| 108 | + | } | |
| 109 | + | } |
| @@ -0,0 +1,65 @@ | |||
| 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 | + | #ifndef RME_MAP_TAB_H_ | |
| 19 | + | #define RME_MAP_TAB_H_ | |
| 20 | + | ||
| 21 | + | #include "editor_tabs.h" | |
| 22 | + | #include "application.h" | |
| 23 | + | #include "map_window.h" | |
| 24 | + | ||
| 25 | + | class MapTab : public EditorTab, public MapWindow { | |
| 26 | + | public: | |
| 27 | + | MapTab(MapTabbook* aui, Editor* editor); | |
| 28 | + | // Constructs a newd window, but it uses the same internal editor as 'other' | |
| 29 | + | // AND the same parent, aui_notebook etc. | |
| 30 | + | MapTab(const MapTab* other); | |
| 31 | + | ~MapTab(); | |
| 32 | + | ||
| 33 | + | bool IsUniqueReference() const; | |
| 34 | + | bool HasSameReference(MapTab* other) const; | |
| 35 | + | ||
| 36 | + | // Properties | |
| 37 | + | MapWindow* GetView() const; | |
| 38 | + | MapCanvas* GetCanvas() const; | |
| 39 | + | wxWindow* GetWindow() const; | |
| 40 | + | wxString GetTitle() const; | |
| 41 | + | Editor* GetEditor() const; | |
| 42 | + | Map* GetMap() const; | |
| 43 | + | ||
| 44 | + | // Position methods | |
| 45 | + | Position GetScreenCenterPosition() const; | |
| 46 | + | ||
| 47 | + | void VisibilityCheck(); | |
| 48 | + | ||
| 49 | + | // Event handlers | |
| 50 | + | void OnSwitchEditorMode(EditorMode mode); | |
| 51 | + | ||
| 52 | + | protected: | |
| 53 | + | struct InternalReference { | |
| 54 | + | Editor* editor; | |
| 55 | + | int owner_count; | |
| 56 | + | }; | |
| 57 | + | MapTabbook* aui; | |
| 58 | + | InternalReference* iref; | |
| 59 | + | }; | |
| 60 | + | ||
| 61 | + | inline bool MapTab::HasSameReference(MapTab* other) const { | |
| 62 | + | return other->iref == iref; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | #endif |
| @@ -0,0 +1,280 @@ | |||
| 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 "map_window.h" | |
| 21 | + | #include "gui.h" | |
| 22 | + | #include "sprites.h" | |
| 23 | + | #include "editor.h" | |
| 24 | + | ||
| 25 | + | MapWindow::MapWindow(wxWindow* parent, Editor& editor) : | |
| 26 | + | wxPanel(parent, PANE_MAIN), | |
| 27 | + | editor(editor), | |
| 28 | + | replaceItemsDialog(nullptr), | |
| 29 | + | islandGeneratorDialog(nullptr) { | |
| 30 | + | int GL_settings[3]; | |
| 31 | + | GL_settings[0] = WX_GL_RGBA; | |
| 32 | + | GL_settings[1] = WX_GL_DOUBLEBUFFER; | |
| 33 | + | GL_settings[2] = 0; | |
| 34 | + | canvas = newd MapCanvas(this, editor, GL_settings); | |
| 35 | + | ||
| 36 | + | vScroll = newd MapScrollBar(this, MAP_WINDOW_VSCROLL, wxVERTICAL, canvas); | |
| 37 | + | hScroll = newd MapScrollBar(this, MAP_WINDOW_HSCROLL, wxHORIZONTAL, canvas); | |
| 38 | + | ||
| 39 | + | gem = newd DCButton(this, MAP_WINDOW_GEM, wxDefaultPosition, DC_BTN_NORMAL, RENDER_SIZE_16x16, EDITOR_SPRITE_SELECTION_GEM); | |
| 40 | + | ||
| 41 | + | wxFlexGridSizer* topsizer = newd wxFlexGridSizer(2, 0, 0); | |
| 42 | + | ||
| 43 | + | topsizer->AddGrowableCol(0); | |
| 44 | + | topsizer->AddGrowableRow(0); | |
| 45 | + | ||
| 46 | + | topsizer->Add(canvas, wxSizerFlags(1).Expand()); | |
| 47 | + | topsizer->Add(vScroll, wxSizerFlags(1).Expand()); | |
| 48 | + | topsizer->Add(hScroll, wxSizerFlags(1).Expand()); | |
| 49 | + | topsizer->Add(gem, wxSizerFlags(1)); | |
| 50 | + | ||
| 51 | + | SetSizerAndFit(topsizer); | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | MapWindow::~MapWindow() { | |
| 55 | + | //// | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | void MapWindow::ShowReplaceItemsDialog(bool selectionOnly) { | |
| 59 | + | if (replaceItemsDialog) { | |
| 60 | + | return; | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | replaceItemsDialog = new ReplaceItemsDialog(this, selectionOnly); | |
| 64 | + | replaceItemsDialog->Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(MapWindow::OnReplaceItemsDialogClose), NULL, this); | |
| 65 | + | replaceItemsDialog->Show(); | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | void MapWindow::CloseReplaceItemsDialog() { | |
| 69 | + | if (replaceItemsDialog) { | |
| 70 | + | replaceItemsDialog->Close(); | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | void MapWindow::OnReplaceItemsDialogClose(wxCloseEvent& event) { | |
| 75 | + | if (replaceItemsDialog) { | |
| 76 | + | replaceItemsDialog->Disconnect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(MapWindow::OnReplaceItemsDialogClose), NULL, this); | |
| 77 | + | replaceItemsDialog->Destroy(); | |
| 78 | + | replaceItemsDialog = nullptr; | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | void MapWindow::ShowIslandGeneratorDialog() { | |
| 83 | + | if (islandGeneratorDialog) { | |
| 84 | + | return; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | islandGeneratorDialog = new IslandGeneratorDialog(this); | |
| 88 | + | islandGeneratorDialog->Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(MapWindow::OnIslandGeneratorDialogClose), NULL, this); | |
| 89 | + | islandGeneratorDialog->Show(); | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | void MapWindow::CloseIslandGeneratorDialog() { | |
| 93 | + | if (islandGeneratorDialog) { | |
| 94 | + | islandGeneratorDialog->Close(); | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | void MapWindow::OnIslandGeneratorDialogClose(wxCloseEvent& event) { | |
| 99 | + | if (islandGeneratorDialog) { | |
| 100 | + | islandGeneratorDialog->Disconnect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(MapWindow::OnIslandGeneratorDialogClose), NULL, this); | |
| 101 | + | islandGeneratorDialog->Destroy(); | |
| 102 | + | islandGeneratorDialog = nullptr; | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | void MapWindow::ShowGroundValidationDialog() { | |
| 107 | + | if (groundValidationDialog) { | |
| 108 | + | return; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | groundValidationDialog = new GroundValidationDialog(this); | |
| 112 | + | if (groundValidationDialog->ShowModal() == wxID_OK) { | |
| 113 | + | // Get validation options | |
| 114 | + | bool validateStack = groundValidationDialog->shouldValidateGroundStack(); | |
| 115 | + | bool generateEmpty = groundValidationDialog->shouldGenerateEmptySurroundedGrounds(); | |
| 116 | + | bool removeDuplicates = groundValidationDialog->shouldRemoveDuplicateGrounds(); | |
| 117 | + | ||
| 118 | + | // Perform validation | |
| 119 | + | uint32_t changes = editor.validateGrounds(validateStack, generateEmpty, removeDuplicates); | |
| 120 | + | ||
| 121 | + | // Show results | |
| 122 | + | wxString message; | |
| 123 | + | message << changes << " ground tiles have been modified."; | |
| 124 | + | g_gui.PopupDialog("Ground Validation", message, wxOK); | |
| 125 | + | ||
| 126 | + | // Refresh view | |
| 127 | + | g_gui.RefreshView(); | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | groundValidationDialog->Destroy(); | |
| 131 | + | groundValidationDialog = nullptr; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | void MapWindow::SetSize(int x, int y, bool center) { | |
| 135 | + | if (x == 0 || y == 0) { | |
| 136 | + | return; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | int windowSizeX; | |
| 140 | + | int windowSizeY; | |
| 141 | + | ||
| 142 | + | canvas->GetSize(&windowSizeX, &windowSizeY); | |
| 143 | + | ||
| 144 | + | hScroll->SetScrollbar(center ? (x - windowSizeX) / 2 : hScroll->GetThumbPosition(), windowSizeX / x, x, windowSizeX / x); | |
| 145 | + | vScroll->SetScrollbar(center ? (y - windowSizeY) / 2 : vScroll->GetThumbPosition(), windowSizeY / y, y, windowSizeX / y); | |
| 146 | + | // wxPanel::SetSize(x, y); | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | void MapWindow::UpdateScrollbars(int nx, int ny) { | |
| 150 | + | // nx and ny are size of this window | |
| 151 | + | hScroll->SetScrollbar(hScroll->GetThumbPosition(), nx / max(1, hScroll->GetRange()), max(1, hScroll->GetRange()), 96); | |
| 152 | + | vScroll->SetScrollbar(vScroll->GetThumbPosition(), ny / max(1, vScroll->GetRange()), max(1, vScroll->GetRange()), 96); | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | void MapWindow::UpdateDialogs(bool show) { | |
| 156 | + | if (replaceItemsDialog) { | |
| 157 | + | replaceItemsDialog->Show(show); | |
| 158 | + | } | |
| 159 | + | if (islandGeneratorDialog) { | |
| 160 | + | islandGeneratorDialog->Show(show); | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | void MapWindow::GetViewStart(int* x, int* y) { | |
| 165 | + | *x = hScroll->GetThumbPosition(); | |
| 166 | + | *y = vScroll->GetThumbPosition(); | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | void MapWindow::GetViewSize(int* x, int* y) { | |
| 170 | + | canvas->GetSize(x, y); | |
| 171 | + | *x *= canvas->GetContentScaleFactor(); | |
| 172 | + | *y *= canvas->GetContentScaleFactor(); | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | void MapWindow::FitToMap() { | |
| 176 | + | SetSize(editor.map.getWidth() * TileSize, editor.map.getHeight() * TileSize, true); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | Position MapWindow::GetScreenCenterPosition() const { | |
| 180 | + | int x, y; | |
| 181 | + | canvas->GetScreenCenter(&x, &y); | |
| 182 | + | return Position(x, y, canvas->GetFloor()); | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | void MapWindow::SetScreenCenterPosition(const Position& position) { | |
| 186 | + | if (position == Position()) { | |
| 187 | + | return; | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | int x = position.x * TileSize; | |
| 191 | + | int y = position.y * TileSize; | |
| 192 | + | if (position.z <= GROUND_LAYER) { | |
| 193 | + | // Compensate for floor offset above ground | |
| 194 | + | x -= (GROUND_LAYER - position.z) * TileSize; | |
| 195 | + | y -= (GROUND_LAYER - position.z) * TileSize; | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | const Position& center = GetScreenCenterPosition(); | |
| 199 | + | if (previous_position != center) { | |
| 200 | + | previous_position.x = center.x; | |
| 201 | + | previous_position.y = center.y; | |
| 202 | + | previous_position.z = center.z; | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | Scroll(x, y, true); | |
| 206 | + | canvas->ChangeFloor(position.z); | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | void MapWindow::GoToPreviousCenterPosition() { | |
| 210 | + | SetScreenCenterPosition(previous_position); | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | void MapWindow::Scroll(int x, int y, bool center) { | |
| 214 | + | if (center) { | |
| 215 | + | int windowSizeX, windowSizeY; | |
| 216 | + | ||
| 217 | + | canvas->GetSize(&windowSizeX, &windowSizeY); | |
| 218 | + | x -= int((windowSizeX * g_gui.GetCurrentZoom()) / 2.0); | |
| 219 | + | y -= int((windowSizeY * g_gui.GetCurrentZoom()) / 2.0); | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | hScroll->SetThumbPosition(x); | |
| 223 | + | vScroll->SetThumbPosition(y); | |
| 224 | + | g_gui.UpdateMinimap(); | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | void MapWindow::ScrollRelative(int x, int y) { | |
| 228 | + | hScroll->SetThumbPosition(hScroll->GetThumbPosition() + x); | |
| 229 | + | vScroll->SetThumbPosition(vScroll->GetThumbPosition() + y); | |
| 230 | + | g_gui.UpdateMinimap(); | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | void MapWindow::OnGem(wxCommandEvent& WXUNUSED(event)) { | |
| 234 | + | g_gui.SwitchMode(); | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | void MapWindow::OnSize(wxSizeEvent& event) { | |
| 238 | + | UpdateScrollbars(event.GetSize().GetWidth(), event.GetSize().GetHeight()); | |
| 239 | + | event.Skip(); | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | void MapWindow::OnScroll(wxScrollEvent& event) { | |
| 243 | + | Refresh(); | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | void MapWindow::OnScrollLineDown(wxScrollEvent& event) { | |
| 247 | + | if (event.GetOrientation() == wxHORIZONTAL) { | |
| 248 | + | ScrollRelative(96, 0); | |
| 249 | + | } else { | |
| 250 | + | ScrollRelative(0, 96); | |
| 251 | + | } | |
| 252 | + | Refresh(); | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | void MapWindow::OnScrollLineUp(wxScrollEvent& event) { | |
| 256 | + | if (event.GetOrientation() == wxHORIZONTAL) { | |
| 257 | + | ScrollRelative(-96, 0); | |
| 258 | + | } else { | |
| 259 | + | ScrollRelative(0, -96); | |
| 260 | + | } | |
| 261 | + | Refresh(); | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | void MapWindow::OnScrollPageDown(wxScrollEvent& event) { | |
| 265 | + | if (event.GetOrientation() == wxHORIZONTAL) { | |
| 266 | + | ScrollRelative(5 * 96, 0); | |
| 267 | + | } else { | |
| 268 | + | ScrollRelative(0, 5 * 96); | |
| 269 | + | } | |
| 270 | + | Refresh(); | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | void MapWindow::OnScrollPageUp(wxScrollEvent& event) { | |
| 274 | + | if (event.GetOrientation() == wxHORIZONTAL) { | |
| 275 | + | ScrollRelative(-5 * 96, 0); | |
| 276 | + | } else { | |
| 277 | + | ScrollRelative(0, -5 * 96); | |
| 278 | + | } | |
| 279 | + | Refresh(); | |
| 280 | + | } |
| @@ -0,0 +1,139 @@ | |||
| 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 | + | #ifndef RME_MAP_WINDOW_H_ | |
| 19 | + | #define RME_MAP_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "position.h" | |
| 22 | + | #include "replace_items_window.h" | |
| 23 | + | #include "island_generator_dialog.h" | |
| 24 | + | #include "ground_validation_dialog.h" | |
| 25 | + | ||
| 26 | + | class MapCanvas; | |
| 27 | + | class DCButton; | |
| 28 | + | ||
| 29 | + | // Map window, a window displaying a map, complete with scrollbars | |
| 30 | + | // and everything. This is the window that's inside each tab in the | |
| 31 | + | // editor. Does NOT control any map rendering or editing at all. | |
| 32 | + | // MapCanvas does that. (mapdisplay.h) | |
| 33 | + | class MapWindow : public wxPanel { | |
| 34 | + | public: | |
| 35 | + | MapWindow(wxWindow* parent, Editor& editor); | |
| 36 | + | virtual ~MapWindow(); | |
| 37 | + | ||
| 38 | + | // Event handlers | |
| 39 | + | void OnSize(wxSizeEvent& event); | |
| 40 | + | void OnScroll(wxScrollEvent& event); | |
| 41 | + | void OnScrollLineDown(wxScrollEvent& event); | |
| 42 | + | void OnScrollLineUp(wxScrollEvent& event); | |
| 43 | + | void OnScrollPageDown(wxScrollEvent& event); | |
| 44 | + | void OnScrollPageUp(wxScrollEvent& event); | |
| 45 | + | void OnGem(wxCommandEvent& event); | |
| 46 | + | ||
| 47 | + | // Custom interface for MapWindow | |
| 48 | + | ||
| 49 | + | // GetViewSize returns the size of the containing canvas, in pixels | |
| 50 | + | void GetViewSize(int* x, int* y); | |
| 51 | + | // Returns the start of the camera on the map, in pixels | |
| 52 | + | // Actually is scroll thumb positions | |
| 53 | + | void GetViewStart(int* x, int* y); | |
| 54 | + | ||
| 55 | + | // Set size of this window (in pixels) | |
| 56 | + | // if center is true, the camera will be moved to the center of the map. | |
| 57 | + | void SetSize(int x, int y, bool center = false); | |
| 58 | + | ||
| 59 | + | // Scroll to the specified, absolute position (in pixels) | |
| 60 | + | void Scroll(int x, int y, bool center = false); | |
| 61 | + | ||
| 62 | + | // Scroll this many pixels in X/Y, relative to current position | |
| 63 | + | void ScrollRelative(int x, int y); | |
| 64 | + | ||
| 65 | + | // Resize scrollbars to fit to the map dimensions | |
| 66 | + | // This needs to be called after updating map height/width | |
| 67 | + | void FitToMap(); | |
| 68 | + | ||
| 69 | + | // Screen position. | |
| 70 | + | Position GetScreenCenterPosition() const; | |
| 71 | + | void SetScreenCenterPosition(const Position& position); | |
| 72 | + | void GoToPreviousCenterPosition(); | |
| 73 | + | ||
| 74 | + | // Return the containing canvas | |
| 75 | + | MapCanvas* GetCanvas() const { | |
| 76 | + | return canvas; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | void ShowReplaceItemsDialog(bool selectionOnly); | |
| 80 | + | void CloseReplaceItemsDialog(); | |
| 81 | + | void OnReplaceItemsDialogClose(wxCloseEvent& event); | |
| 82 | + | ||
| 83 | + | void ShowIslandGeneratorDialog(); | |
| 84 | + | void CloseIslandGeneratorDialog(); | |
| 85 | + | void OnIslandGeneratorDialogClose(wxCloseEvent& event); | |
| 86 | + | ||
| 87 | + | void ShowGroundValidationDialog(); | |
| 88 | + | void CloseGroundValidationDialog(); | |
| 89 | + | void OnGroundValidationDialogClose(wxCloseEvent& event); | |
| 90 | + | ||
| 91 | + | protected: | |
| 92 | + | // For internal use, call to resize the scrollbars with | |
| 93 | + | // the newd dimensions of *this* window | |
| 94 | + | void UpdateScrollbars(int nx, int ny); | |
| 95 | + | void UpdateDialogs(bool show); | |
| 96 | + | ||
| 97 | + | protected: | |
| 98 | + | Editor& editor; | |
| 99 | + | DCButton* gem; | |
| 100 | + | MapCanvas* canvas; | |
| 101 | + | wxScrollBar* hScroll; | |
| 102 | + | wxScrollBar* vScroll; | |
| 103 | + | ||
| 104 | + | private: | |
| 105 | + | ReplaceItemsDialog* replaceItemsDialog; | |
| 106 | + | IslandGeneratorDialog* islandGeneratorDialog; | |
| 107 | + | GroundValidationDialog* groundValidationDialog; | |
| 108 | + | Position previous_position; | |
| 109 | + | ||
| 110 | + | friend class MainFrame; | |
| 111 | + | friend class MapCanvas; | |
| 112 | + | ||
| 113 | + | DECLARE_EVENT_TABLE() | |
| 114 | + | }; | |
| 115 | + | ||
| 116 | + | // MapScrollbar, a special scrollbar that relays alot of events | |
| 117 | + | // to the canvas, which allows scrolling when the scrollbar has | |
| 118 | + | // focus (even though it also resents focus as hard as it can. | |
| 119 | + | class MapScrollBar : public wxScrollBar { | |
| 120 | + | public: | |
| 121 | + | MapScrollBar(MapWindow* parent, wxWindowID id, long style, wxWindow* canvas) : | |
| 122 | + | wxScrollBar(parent, id, wxDefaultPosition, wxDefaultSize, style), canvas(canvas) { } | |
| 123 | + | virtual ~MapScrollBar() { } | |
| 124 | + | ||
| 125 | + | void OnKey(wxKeyEvent& event) { | |
| 126 | + | canvas->GetEventHandler()->AddPendingEvent(event); | |
| 127 | + | } | |
| 128 | + | void OnWheel(wxMouseEvent& event) { | |
| 129 | + | canvas->GetEventHandler()->AddPendingEvent(event); | |
| 130 | + | } | |
| 131 | + | void OnFocus(wxFocusEvent& event) { | |
| 132 | + | canvas->SetFocus(); | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | wxWindow* canvas; | |
| 136 | + | DECLARE_EVENT_TABLE() | |
| 137 | + | }; | |
| 138 | + | ||
| 139 | + | #endif |
| @@ -0,0 +1,381 @@ | |||
| 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 <wx/dir.h> | |
| 21 | + | ||
| 22 | + | #include "editor.h" | |
| 23 | + | #include "items.h" | |
| 24 | + | #include "creatures.h" | |
| 25 | + | ||
| 26 | + | #include "gui.h" | |
| 27 | + | #include "materials.h" | |
| 28 | + | #include "brush.h" | |
| 29 | + | #include "creature_brush.h" | |
| 30 | + | #include "raw_brush.h" | |
| 31 | + | ||
| 32 | + | Materials g_materials; | |
| 33 | + | ||
| 34 | + | Materials::Materials() { | |
| 35 | + | //// | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | Materials::~Materials() { | |
| 39 | + | clear(); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | void Materials::clear() { | |
| 43 | + | for (TilesetContainer::iterator iter = tilesets.begin(); iter != tilesets.end(); ++iter) { | |
| 44 | + | delete iter->second; | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | for (MaterialsExtensionList::iterator iter = extensions.begin(); iter != extensions.end(); ++iter) { | |
| 48 | + | delete *iter; | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | tilesets.clear(); | |
| 52 | + | extensions.clear(); | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | const MaterialsExtensionList& Materials::getExtensions() { | |
| 56 | + | return extensions; | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | MaterialsExtensionList Materials::getExtensionsByVersion(uint16_t version_id) { | |
| 60 | + | MaterialsExtensionList ret_list; | |
| 61 | + | for (MaterialsExtensionList::iterator iter = extensions.begin(); iter != extensions.end(); ++iter) { | |
| 62 | + | if ((*iter)->isForVersion(version_id)) { | |
| 63 | + | ret_list.push_back(*iter); | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | return ret_list; | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | bool Materials::loadMaterials(const FileName& identifier, wxString& error, wxArrayString& warnings) { | |
| 70 | + | pugi::xml_document doc; | |
| 71 | + | pugi::xml_parse_result result = doc.load_file(identifier.GetFullPath().mb_str()); | |
| 72 | + | if (!result) { | |
| 73 | + | warnings.push_back("Could not open " + identifier.GetFullName() + " (file not found or syntax error)"); | |
| 74 | + | return false; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | pugi::xml_node node = doc.child("materials"); | |
| 78 | + | if (!node) { | |
| 79 | + | warnings.push_back(identifier.GetFullName() + ": Invalid rootheader."); | |
| 80 | + | return false; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | unserializeMaterials(identifier, node, error, warnings); | |
| 84 | + | return true; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | bool Materials::loadExtensions(FileName directoryName, wxString& error, wxArrayString& warnings) { | |
| 88 | + | directoryName.Mkdir(0755, wxPATH_MKDIR_FULL); // Create if it doesn't exist | |
| 89 | + | ||
| 90 | + | wxDir ext_dir(directoryName.GetPath()); | |
| 91 | + | if (!ext_dir.IsOpened()) { | |
| 92 | + | error = "Could not open extensions directory."; | |
| 93 | + | return false; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | wxString filename; | |
| 97 | + | if (!ext_dir.GetFirst(&filename)) { | |
| 98 | + | // No extensions found | |
| 99 | + | return true; | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | StringVector clientVersions; | |
| 103 | + | do { | |
| 104 | + | FileName fn; | |
| 105 | + | fn.SetPath(directoryName.GetPath()); | |
| 106 | + | fn.SetFullName(filename); | |
| 107 | + | if (fn.GetExt() != "xml") { | |
| 108 | + | continue; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | pugi::xml_document doc; | |
| 112 | + | pugi::xml_parse_result result = doc.load_file(fn.GetFullPath().mb_str()); | |
| 113 | + | if (!result) { | |
| 114 | + | warnings.push_back("Could not open " + filename + " (file not found or syntax error)"); | |
| 115 | + | continue; | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | pugi::xml_node extensionNode = doc.child("materialsextension"); | |
| 119 | + | if (!extensionNode) { | |
| 120 | + | warnings.push_back(filename + ": Invalid rootheader."); | |
| 121 | + | continue; | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | pugi::xml_attribute attribute; | |
| 125 | + | if (!(attribute = extensionNode.attribute("name"))) { | |
| 126 | + | warnings.push_back(filename + ": Couldn't read extension name."); | |
| 127 | + | continue; | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | const std::string& extensionName = attribute.as_string(); | |
| 131 | + | if (!(attribute = extensionNode.attribute("author"))) { | |
| 132 | + | warnings.push_back(filename + ": Couldn't read extension name."); | |
| 133 | + | continue; | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | const std::string& extensionAuthor = attribute.as_string(); | |
| 137 | + | if (!(attribute = extensionNode.attribute("description"))) { | |
| 138 | + | warnings.push_back(filename + ": Couldn't read extension name."); | |
| 139 | + | continue; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | const std::string& extensionDescription = attribute.as_string(); | |
| 143 | + | if (extensionName.empty() || extensionAuthor.empty() || extensionDescription.empty()) { | |
| 144 | + | warnings.push_back(filename + ": Couldn't read extension attributes (name, author, description)."); | |
| 145 | + | continue; | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | std::string extensionUrl = extensionNode.attribute("url").as_string(); | |
| 149 | + | extensionUrl.erase(std::remove(extensionUrl.begin(), extensionUrl.end(), '\'')); | |
| 150 | + | ||
| 151 | + | std::string extensionAuthorLink = extensionNode.attribute("authorurl").as_string(); | |
| 152 | + | extensionAuthorLink.erase(std::remove(extensionAuthorLink.begin(), extensionAuthorLink.end(), '\'')); | |
| 153 | + | ||
| 154 | + | MaterialsExtension* materialExtension = newd MaterialsExtension(extensionName, extensionAuthor, extensionDescription); | |
| 155 | + | materialExtension->url = extensionUrl; | |
| 156 | + | materialExtension->author_url = extensionAuthorLink; | |
| 157 | + | ||
| 158 | + | if ((attribute = extensionNode.attribute("client"))) { | |
| 159 | + | clientVersions.clear(); | |
| 160 | + | const std::string& extensionClientString = attribute.as_string(); | |
| 161 | + | ||
| 162 | + | size_t lastPosition = 0; | |
| 163 | + | size_t position = extensionClientString.find(';'); | |
| 164 | + | while (position != std::string::npos) { | |
| 165 | + | clientVersions.push_back(extensionClientString.substr(lastPosition, position - lastPosition)); | |
| 166 | + | lastPosition = position + 1; | |
| 167 | + | position = extensionClientString.find(';', lastPosition); | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | clientVersions.push_back(extensionClientString.substr(lastPosition)); | |
| 171 | + | for (const std::string& version : clientVersions) { | |
| 172 | + | materialExtension->addVersion(version); | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | std::sort(materialExtension->version_list.begin(), materialExtension->version_list.end(), VersionComparisonPredicate); | |
| 176 | + | ||
| 177 | + | auto duplicate = std::unique(materialExtension->version_list.begin(), materialExtension->version_list.end()); | |
| 178 | + | while (duplicate != materialExtension->version_list.end()) { | |
| 179 | + | materialExtension->version_list.erase(duplicate); | |
| 180 | + | duplicate = std::unique(materialExtension->version_list.begin(), materialExtension->version_list.end()); | |
| 181 | + | } | |
| 182 | + | } else { | |
| 183 | + | warnings.push_back(filename + ": Extension is not available for any version."); | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | extensions.push_back(materialExtension); | |
| 187 | + | if (materialExtension->isForVersion(g_gui.GetCurrentVersionID())) { | |
| 188 | + | unserializeMaterials(filename, extensionNode, error, warnings); | |
| 189 | + | } | |
| 190 | + | } while (ext_dir.GetNext(&filename)); | |
| 191 | + | ||
| 192 | + | return true; | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | bool Materials::unserializeMaterials(const FileName& filename, pugi::xml_node node, wxString& error, wxArrayString& warnings) { | |
| 196 | + | wxString warning; | |
| 197 | + | pugi::xml_attribute attribute; | |
| 198 | + | for (pugi::xml_node childNode = node.first_child(); childNode; childNode = childNode.next_sibling()) { | |
| 199 | + | const std::string& childName = as_lower_str(childNode.name()); | |
| 200 | + | if (childName == "include") { | |
| 201 | + | if (!(attribute = childNode.attribute("file"))) { | |
| 202 | + | continue; | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | FileName includeName; | |
| 206 | + | includeName.SetPath(filename.GetPath()); | |
| 207 | + | includeName.SetFullName(wxString(attribute.as_string(), wxConvUTF8)); | |
| 208 | + | ||
| 209 | + | wxString subError; | |
| 210 | + | if (!loadMaterials(includeName, subError, warnings)) { | |
| 211 | + | warnings.push_back("Error while loading file \"" + includeName.GetFullName() + "\": " + subError); | |
| 212 | + | } | |
| 213 | + | } else if (childName == "metaitem") { | |
| 214 | + | g_items.loadMetaItem(childNode); | |
| 215 | + | } else if (childName == "border") { | |
| 216 | + | g_brushes.unserializeBorder(childNode, warnings); | |
| 217 | + | if (warning.size()) { | |
| 218 | + | warnings.push_back("materials.xml: " + warning); | |
| 219 | + | } | |
| 220 | + | } else if (childName == "brush") { | |
| 221 | + | g_brushes.unserializeBrush(childNode, warnings); | |
| 222 | + | if (warning.size()) { | |
| 223 | + | warnings.push_back("materials.xml: " + warning); | |
| 224 | + | } | |
| 225 | + | } else if (childName == "tileset") { | |
| 226 | + | unserializeTileset(childNode, warnings); | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | return true; | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | void Materials::createOtherTileset() { | |
| 233 | + | Tileset* others; | |
| 234 | + | Tileset* npc_tileset; | |
| 235 | + | ||
| 236 | + | if (tilesets["Others"] != nullptr) { | |
| 237 | + | others = tilesets["Others"]; | |
| 238 | + | others->clear(); | |
| 239 | + | } else { | |
| 240 | + | others = newd Tileset(g_brushes, "Others"); | |
| 241 | + | tilesets["Others"] = others; | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | if (tilesets["NPCs"] != nullptr) { | |
| 245 | + | npc_tileset = tilesets["NPCs"]; | |
| 246 | + | npc_tileset->clear(); | |
| 247 | + | } else { | |
| 248 | + | npc_tileset = newd Tileset(g_brushes, "NPCs"); | |
| 249 | + | tilesets["NPCs"] = npc_tileset; | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // There should really be an iterator to do this | |
| 253 | + | for (int32_t id = 0; id <= g_items.getMaxID(); ++id) { | |
| 254 | + | ItemType& it = g_items[id]; | |
| 255 | + | if (it.id == 0) { | |
| 256 | + | continue; | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | if (!it.isMetaItem()) { | |
| 260 | + | Brush* brush; | |
| 261 | + | if (it.in_other_tileset) { | |
| 262 | + | others->getCategory(TILESET_RAW)->brushlist.push_back(it.raw_brush); | |
| 263 | + | continue; | |
| 264 | + | } else if (it.raw_brush == nullptr) { | |
| 265 | + | brush = it.raw_brush = newd RAWBrush(it.id); | |
| 266 | + | it.has_raw = true; | |
| 267 | + | g_brushes.addBrush(it.raw_brush); | |
| 268 | + | } else if (!it.has_raw) { | |
| 269 | + | brush = it.raw_brush; | |
| 270 | + | } else { | |
| 271 | + | continue; | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | brush->flagAsVisible(); | |
| 275 | + | others->getCategory(TILESET_RAW)->brushlist.push_back(it.raw_brush); | |
| 276 | + | it.in_other_tileset = true; | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | for (CreatureMap::iterator iter = g_creatures.begin(); iter != g_creatures.end(); ++iter) { | |
| 281 | + | CreatureType* type = iter->second; | |
| 282 | + | if (type->in_other_tileset) { | |
| 283 | + | if (type->isNpc) { | |
| 284 | + | npc_tileset->getCategory(TILESET_CREATURE)->brushlist.push_back(type->brush); | |
| 285 | + | } else { | |
| 286 | + | others->getCategory(TILESET_CREATURE)->brushlist.push_back(type->brush); | |
| 287 | + | } | |
| 288 | + | } else if (type->brush == nullptr) { | |
| 289 | + | type->brush = newd CreatureBrush(type); | |
| 290 | + | g_brushes.addBrush(type->brush); | |
| 291 | + | type->brush->flagAsVisible(); | |
| 292 | + | type->in_other_tileset = true; | |
| 293 | + | if (type->isNpc) { | |
| 294 | + | npc_tileset->getCategory(TILESET_CREATURE)->brushlist.push_back(type->brush); | |
| 295 | + | } else { | |
| 296 | + | others->getCategory(TILESET_CREATURE)->brushlist.push_back(type->brush); | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | bool Materials::unserializeTileset(pugi::xml_node node, wxArrayString& warnings) { | |
| 303 | + | pugi::xml_attribute attribute; | |
| 304 | + | if (!(attribute = node.attribute("name"))) { | |
| 305 | + | warnings.push_back("Couldn't read tileset name"); | |
| 306 | + | return false; | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | const std::string& name = attribute.as_string(); | |
| 310 | + | ||
| 311 | + | Tileset* tileset; | |
| 312 | + | auto it = tilesets.find(name); | |
| 313 | + | if (it != tilesets.end()) { | |
| 314 | + | tileset = it->second; | |
| 315 | + | } else { | |
| 316 | + | tileset = newd Tileset(g_brushes, name); | |
| 317 | + | tilesets.insert(std::make_pair(name, tileset)); | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | for (pugi::xml_node childNode = node.first_child(); childNode; childNode = childNode.next_sibling()) { | |
| 321 | + | tileset->loadCategory(childNode, warnings); | |
| 322 | + | } | |
| 323 | + | return true; | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | void Materials::addToTileset(std::string tilesetName, int itemId, TilesetCategoryType categoryType) { | |
| 327 | + | ItemType& it = g_items[itemId]; | |
| 328 | + | ||
| 329 | + | if (it.id == 0) { | |
| 330 | + | return; | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | Tileset* tileset; | |
| 334 | + | auto _it = tilesets.find(tilesetName); | |
| 335 | + | if (_it != tilesets.end()) { | |
| 336 | + | tileset = _it->second; | |
| 337 | + | } else { | |
| 338 | + | tileset = newd Tileset(g_brushes, tilesetName); | |
| 339 | + | tilesets.insert(std::make_pair(tilesetName, tileset)); | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | TilesetCategory* category = tileset->getCategory(categoryType); | |
| 343 | + | ||
| 344 | + | if (!it.isMetaItem()) { | |
| 345 | + | Brush* brush; | |
| 346 | + | if (it.in_other_tileset) { | |
| 347 | + | category->brushlist.push_back(it.raw_brush); | |
| 348 | + | return; | |
| 349 | + | } else if (it.raw_brush == nullptr) { | |
| 350 | + | brush = it.raw_brush = newd RAWBrush(it.id); | |
| 351 | + | it.has_raw = true; | |
| 352 | + | g_brushes.addBrush(it.raw_brush); | |
| 353 | + | } else { | |
| 354 | + | brush = it.raw_brush; | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | brush->flagAsVisible(); | |
| 358 | + | category->brushlist.push_back(it.raw_brush); | |
| 359 | + | it.in_other_tileset = true; | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | bool Materials::isInTileset(Item* item, std::string tilesetName) const { | |
| 364 | + | const ItemType& it = g_items[item->getID()]; | |
| 365 | + | ||
| 366 | + | return it.id != 0 && (isInTileset(it.brush, tilesetName) || isInTileset(it.doodad_brush, tilesetName) || isInTileset(it.raw_brush, tilesetName)) || isInTileset(it.collection_brush, tilesetName); | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | bool Materials::isInTileset(Brush* brush, std::string tilesetName) const { | |
| 370 | + | if (!brush) { | |
| 371 | + | return false; | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | TilesetContainer::const_iterator tilesetiter = tilesets.find(tilesetName); | |
| 375 | + | if (tilesetiter == tilesets.end()) { | |
| 376 | + | return false; | |
| 377 | + | } | |
| 378 | + | Tileset* tileset = tilesetiter->second; | |
| 379 | + | ||
| 380 | + | return tileset->containsBrush(brush); | |
| 381 | + | } |
| @@ -0,0 +1,64 @@ | |||
| 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 | + | #ifndef RME_MATERIALS_H_ | |
| 19 | + | #define RME_MATERIALS_H_ | |
| 20 | + | ||
| 21 | + | #include "extension.h" | |
| 22 | + | ||
| 23 | + | class Materials { | |
| 24 | + | public: | |
| 25 | + | Materials(); | |
| 26 | + | ~Materials(); | |
| 27 | + | ||
| 28 | + | void clear(); | |
| 29 | + | ||
| 30 | + | const MaterialsExtensionList& getExtensions(); | |
| 31 | + | MaterialsExtensionList getExtensionsByVersion(uint16_t version_id); | |
| 32 | + | ||
| 33 | + | TilesetContainer tilesets; | |
| 34 | + | ||
| 35 | + | bool loadMaterials(const FileName& identifier, wxString& error, wxArrayString& warnings); | |
| 36 | + | bool loadExtensions(FileName identifier, wxString& error, wxArrayString& warnings); | |
| 37 | + | void createOtherTileset(); | |
| 38 | + | void addToTileset(std::string tilesetName, int itemId, TilesetCategoryType categoryType); | |
| 39 | + | ||
| 40 | + | bool isInTileset(Item* item, std::string tileset) const; | |
| 41 | + | bool isInTileset(Brush* brush, std::string tileset) const; | |
| 42 | + | bool needSave() const { | |
| 43 | + | return modified; | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | void modify(bool newValue = true) { | |
| 47 | + | this->modified = newValue; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | protected: | |
| 51 | + | bool unserializeMaterials(const FileName& filename, pugi::xml_node node, wxString& error, wxArrayString& warnings); | |
| 52 | + | bool unserializeTileset(pugi::xml_node node, wxArrayString& warnings); | |
| 53 | + | ||
| 54 | + | MaterialsExtensionList extensions; | |
| 55 | + | ||
| 56 | + | private: | |
| 57 | + | bool modified = false; | |
| 58 | + | Materials(const Materials&); | |
| 59 | + | Materials& operator=(const Materials&); | |
| 60 | + | }; | |
| 61 | + | ||
| 62 | + | extern Materials g_materials; | |
| 63 | + | ||
| 64 | + | #endif |
| @@ -0,0 +1,925 @@ | |||
| 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 | + | CURRENT SITUATION: | |
| 19 | + | 1. Minimap refresh issues: | |
| 20 | + | - Updates when changing floors (minimap_window.cpp:93-101) | |
| 21 | + | - Does not update properly after paste operations (copybuffer.cpp:232-257) | |
| 22 | + | - No caching mechanism for faster reloading | |
| 23 | + | - No binary file storage for minimap data | |
| 24 | + | ||
| 25 | + | IMPROVEMENT TASKS: | |
| 26 | + | 1. Paste Operation Minimap Update: | |
| 27 | + | - Add minimap update calls in copybuffer.cpp after paste operations | |
| 28 | + | - Track modified positions during paste for efficient updates | |
| 29 | + | - Reference: copybuffer.cpp:232-257 | |
| 30 | + | ||
| 31 | + | 2. Minimap Caching: | |
| 32 | + | - Implement block-based caching system (already started in MinimapBlock struct) | |
| 33 | + | - Add floor-specific caching | |
| 34 | + | - Optimize block size for better performance (currently 256) | |
| 35 | + | - Reference: minimap_window.cpp:53-55 | |
| 36 | + | ||
| 37 | + | 3. Binary File Storage: | |
| 38 | + | - Create binary file format for minimap data | |
| 39 | + | - Store color information and floor data | |
| 40 | + | - Implement save/load functions | |
| 41 | + | - Cache binary data for quick loading | |
| 42 | + | ||
| 43 | + | 4. Drawing Optimization: | |
| 44 | + | - Batch rendering operations | |
| 45 | + | - Implement dirty region tracking | |
| 46 | + | - Use hardware acceleration where possible | |
| 47 | + | - Reference: minimap_window.cpp:290-339 | |
| 48 | + | ||
| 49 | + | RELEVANT CODE SECTIONS: | |
| 50 | + | - Minimap window core: minimap_window.cpp:82-177 | |
| 51 | + | - Copy buffer paste: copybuffer.cpp:232-257 | |
| 52 | + | - Selection handling: selection.cpp:1-365 | |
| 53 | + | */ | |
| 54 | + | #include "main.h" | |
| 55 | + | ||
| 56 | + | #include "graphics.h" | |
| 57 | + | #include "editor.h" | |
| 58 | + | #include "map.h" | |
| 59 | + | ||
| 60 | + | #include "gui.h" | |
| 61 | + | #include "map_display.h" | |
| 62 | + | #include "minimap_window.h" | |
| 63 | + | ||
| 64 | + | #include <thread> | |
| 65 | + | #include <mutex> | |
| 66 | + | #include <atomic> | |
| 67 | + | #include <wx/filename.h> | |
| 68 | + | #include <wx/image.h> | |
| 69 | + | #include <wx/dir.h> | |
| 70 | + | #include <wx/ffile.h> | |
| 71 | + | ||
| 72 | + | BEGIN_EVENT_TABLE(MinimapWindow, wxPanel) | |
| 73 | + | EVT_PAINT(MinimapWindow::OnPaint) | |
| 74 | + | EVT_ERASE_BACKGROUND(MinimapWindow::OnEraseBackground) | |
| 75 | + | EVT_LEFT_DOWN(MinimapWindow::OnMouseClick) | |
| 76 | + | EVT_KEY_DOWN(MinimapWindow::OnKey) | |
| 77 | + | EVT_SIZE(MinimapWindow::OnSize) | |
| 78 | + | EVT_CLOSE(MinimapWindow::OnClose) | |
| 79 | + | EVT_TIMER(ID_MINIMAP_UPDATE, MinimapWindow::OnDelayedUpdate) | |
| 80 | + | EVT_TIMER(ID_RESIZE_TIMER, MinimapWindow::OnResizeTimer) | |
| 81 | + | ||
| 82 | + | END_EVENT_TABLE() | |
| 83 | + | ||
| 84 | + | MinimapWindow::MinimapWindow(wxWindow* parent) : | |
| 85 | + | wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(205, 130), wxFULL_REPAINT_ON_RESIZE), | |
| 86 | + | update_timer(this), | |
| 87 | + | thread_running(false), | |
| 88 | + | needs_update(true), | |
| 89 | + | last_center_x(0), | |
| 90 | + | last_center_y(0), | |
| 91 | + | last_floor(0), | |
| 92 | + | last_start_x(0), | |
| 93 | + | last_start_y(0), | |
| 94 | + | is_resizing(false), | |
| 95 | + | empty_tile_atlas_initialized(false) | |
| 96 | + | { | |
| 97 | + | for (int i = 0; i < 256; ++i) { | |
| 98 | + | pens[i] = newd wxPen(wxColor(minimap_color[i].red, minimap_color[i].green, minimap_color[i].blue)); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // Initialize the update timer | |
| 102 | + | update_timer.SetOwner(this, ID_MINIMAP_UPDATE); | |
| 103 | + | ||
| 104 | + | // Initialize the resize timer | |
| 105 | + | resize_timer.SetOwner(this, ID_RESIZE_TIMER); | |
| 106 | + | ||
| 107 | + | // Floor initialization fix | |
| 108 | + | if (g_gui.IsEditorOpen()) { | |
| 109 | + | minimap_floor = g_gui.GetCurrentFloor(); | |
| 110 | + | } else { | |
| 111 | + | minimap_floor = 7; // Safe default | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // Minimap waypoint UI | |
| 115 | + | minimap_waypoint_combo = new wxComboBox(this, wxID_ANY, wxEmptyString, wxPoint(-100, -100), wxSize(120, 22)); | |
| 116 | + | add_minimap_waypoint_btn = new wxButton(this, wxID_ANY, "+", wxPoint(-100, -100), wxSize(28, 22)); | |
| 117 | + | save_minimap_waypoints_btn = new wxButton(this, wxID_ANY, "Save", wxPoint(-100, -100), wxSize(48, 22)); | |
| 118 | + | load_minimap_waypoints_btn = new wxButton(this, wxID_ANY, "Load", wxPoint(-100, -100), wxSize(48, 22)); | |
| 119 | + | minimap_waypoint_combo->Bind(wxEVT_COMBOBOX, &MinimapWindow::OnMinimapWaypointSelected, this); | |
| 120 | + | add_minimap_waypoint_btn->Bind(wxEVT_BUTTON, &MinimapWindow::OnAddMinimapWaypoint, this); | |
| 121 | + | save_minimap_waypoints_btn->Bind(wxEVT_BUTTON, &MinimapWindow::OnSaveMinimapWaypoints, this); | |
| 122 | + | load_minimap_waypoints_btn->Bind(wxEVT_BUTTON, &MinimapWindow::OnLoadMinimapWaypoints, this); | |
| 123 | + | UpdateMinimapWaypointCombo(); | |
| 124 | + | ||
| 125 | + | // In constructor, create save_cache_checkbox and bind event | |
| 126 | + | save_cache_checkbox = new wxCheckBox(this, wxID_ANY, "Save cache to disk", wxPoint(-100, -100), wxSize(130, 22)); | |
| 127 | + | save_cache_checkbox->SetValue(false); | |
| 128 | + | save_cache_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { | |
| 129 | + | save_cache_to_disk = save_cache_checkbox->GetValue(); | |
| 130 | + | }); | |
| 131 | + | ||
| 132 | + | StartRenderThread(); | |
| 133 | + | ||
| 134 | + | // Schedule initial loading after a short delay | |
| 135 | + | update_timer.Start(100, true); // Start a one-shot timer for 100ms | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | MinimapWindow::~MinimapWindow() { | |
| 139 | + | StopRenderThread(); | |
| 140 | + | for (int i = 0; i < 256; ++i) { | |
| 141 | + | delete pens[i]; | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | void MinimapWindow::StartRenderThread() { | |
| 146 | + | thread_running = true; | |
| 147 | + | render_thread = std::thread(&MinimapWindow::RenderThreadFunction, this); | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | void MinimapWindow::StopRenderThread() { | |
| 151 | + | thread_running = false; | |
| 152 | + | if(render_thread.joinable()) { | |
| 153 | + | render_thread.join(); | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | void MinimapWindow::RenderThreadFunction() { | |
| 158 | + | while(thread_running) { | |
| 159 | + | if(needs_update && g_gui.IsEditorOpen()) { | |
| 160 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 161 | + | MapCanvas* canvas = g_gui.GetCurrentMapTab()->GetCanvas(); | |
| 162 | + | ||
| 163 | + | int center_x, center_y; | |
| 164 | + | canvas->GetScreenCenter(¢er_x, ¢er_y); | |
| 165 | + | int floor = g_gui.GetCurrentFloor(); | |
| 166 | + | ||
| 167 | + | // Force update if floor changed | |
| 168 | + | if(floor != last_floor) { | |
| 169 | + | // Clear the buffer when floor changes | |
| 170 | + | std::lock_guard<std::mutex> lock(buffer_mutex); | |
| 171 | + | buffer = wxBitmap(GetSize().GetWidth(), GetSize().GetHeight()); | |
| 172 | + | ||
| 173 | + | // Clear block cache | |
| 174 | + | std::lock_guard<std::mutex> blockLock(m_mutex); | |
| 175 | + | m_blocks.clear(); | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // Always update if floor changed or position changed | |
| 179 | + | if(center_x != last_center_x || | |
| 180 | + | center_y != last_center_y || | |
| 181 | + | floor != last_floor) { | |
| 182 | + | ||
| 183 | + | int window_width = GetSize().GetWidth(); | |
| 184 | + | int window_height = GetSize().GetHeight(); | |
| 185 | + | ||
| 186 | + | // Create temporary bitmap | |
| 187 | + | wxBitmap temp_buffer(window_width, window_height); | |
| 188 | + | wxMemoryDC dc(temp_buffer); | |
| 189 | + | ||
| 190 | + | dc.SetBackground(*wxBLACK_BRUSH); | |
| 191 | + | dc.Clear(); | |
| 192 | + | ||
| 193 | + | int start_x = center_x - window_width / 2; | |
| 194 | + | int start_y = center_y - window_height / 2; | |
| 195 | + | ||
| 196 | + | // Batch drawing by color | |
| 197 | + | std::vector<std::vector<wxPoint>> colorPoints(256); | |
| 198 | + | ||
| 199 | + | for(int y = 0; y < window_height; ++y) { | |
| 200 | + | for(int x = 0; x < window_width; ++x) { | |
| 201 | + | int map_x = start_x + x; | |
| 202 | + | int map_y = start_y + y; | |
| 203 | + | ||
| 204 | + | if(map_x >= 0 && map_y >= 0 && | |
| 205 | + | map_x < editor.map.getWidth() && | |
| 206 | + | map_y < editor.map.getHeight()) { | |
| 207 | + | ||
| 208 | + | Tile* tile = editor.map.getTile(map_x, map_y, floor); | |
| 209 | + | if(tile) { | |
| 210 | + | uint8_t color = tile->getMiniMapColor(); | |
| 211 | + | if(color) { | |
| 212 | + | colorPoints[color].push_back(wxPoint(x, y)); | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // Draw points by color | |
| 220 | + | for(int color = 0; color < 256; ++color) { | |
| 221 | + | if(!colorPoints[color].empty()) { | |
| 222 | + | dc.SetPen(*pens[color]); | |
| 223 | + | for(const wxPoint& pt : colorPoints[color]) { | |
| 224 | + | dc.DrawPoint(pt.x, pt.y); | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // Update buffer safely | |
| 230 | + | { | |
| 231 | + | std::lock_guard<std::mutex> lock(buffer_mutex); | |
| 232 | + | buffer = temp_buffer; | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // Store current state | |
| 236 | + | last_center_x = center_x; | |
| 237 | + | last_center_y = center_y; | |
| 238 | + | last_floor = floor; | |
| 239 | + | ||
| 240 | + | // Request refresh of the window | |
| 241 | + | wxCommandEvent evt(wxEVT_COMMAND_BUTTON_CLICKED); | |
| 242 | + | evt.SetId(ID_MINIMAP_UPDATE); | |
| 243 | + | wxPostEvent(this, evt); | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | needs_update = false; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // Sleep to prevent excessive CPU usage | |
| 250 | + | std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | void MinimapWindow::OnSize(wxSizeEvent& event) { | |
| 255 | + | // Get the new size | |
| 256 | + | wxSize newSize = event.GetSize(); | |
| 257 | + | ||
| 258 | + | // If we're already at this size, skip | |
| 259 | + | if (newSize == GetSize()) { | |
| 260 | + | event.Skip(); | |
| 261 | + | return; | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // Mark that we're resizing | |
| 265 | + | is_resizing = true; | |
| 266 | + | ||
| 267 | + | // Stop any previous resize timer | |
| 268 | + | if (resize_timer.IsRunning()) { | |
| 269 | + | resize_timer.Stop(); | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // Create a new buffer at the new size | |
| 273 | + | { | |
| 274 | + | std::lock_guard<std::mutex> lock(buffer_mutex); | |
| 275 | + | buffer = wxBitmap(newSize.GetWidth(), newSize.GetHeight()); | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | // Clear the block cache since we're changing size | |
| 279 | + | { | |
| 280 | + | std::lock_guard<std::mutex> lock(m_mutex); | |
| 281 | + | m_blocks.clear(); | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // Start the resize timer (will fire when resize is complete) | |
| 285 | + | resize_timer.Start(50, true); // Reduced to 50ms for faster response | |
| 286 | + | ||
| 287 | + | // Skip the event to allow default handling | |
| 288 | + | event.Skip(); | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | void MinimapWindow::OnClose(wxCloseEvent& event) { | |
| 292 | + | // Hide the window instead of destroying it | |
| 293 | + | // This allows the minimap to be reopened in the same position | |
| 294 | + | if (wxWindow::GetParent()) { | |
| 295 | + | g_gui.HideMinimap(); | |
| 296 | + | event.Veto(); // Prevent the window from being destroyed | |
| 297 | + | } else { | |
| 298 | + | event.Skip(); // Allow default handling if no parent | |
| 299 | + | } | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | void MinimapWindow::OnDelayedUpdate(wxTimerEvent& event) { | |
| 303 | + | if (g_gui.IsEditorOpen()) { | |
| 304 | + | // If editor is already open, load the minimap | |
| 305 | + | if (event.GetId() == ID_MINIMAP_UPDATE) { | |
| 306 | + | InitialLoad(); | |
| 307 | + | } | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | needs_update = true; | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | void MinimapWindow::DelayedUpdate() { | |
| 314 | + | update_timer.Start(100, true); // 100ms single-shot timer | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | void MinimapWindow::OnResizeTimer(wxTimerEvent& event) { | |
| 318 | + | // Resizing has stopped | |
| 319 | + | is_resizing = false; | |
| 320 | + | ||
| 321 | + | // Force a complete redraw with the new size | |
| 322 | + | needs_update = true; | |
| 323 | + | ||
| 324 | + | // Request a full refresh | |
| 325 | + | Refresh(); | |
| 326 | + | ||
| 327 | + | // Trigger an immediate update | |
| 328 | + | if (g_gui.IsEditorOpen()) { | |
| 329 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 330 | + | MapCanvas* canvas = g_gui.GetCurrentMapTab()->GetCanvas(); | |
| 331 | + | ||
| 332 | + | int center_x, center_y; | |
| 333 | + | canvas->GetScreenCenter(¢er_x, ¢er_y); | |
| 334 | + | ||
| 335 | + | // Force update of the render thread | |
| 336 | + | last_center_x = center_x; | |
| 337 | + | last_center_y = center_y; | |
| 338 | + | last_floor = g_gui.GetCurrentFloor(); | |
| 339 | + | } | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | void MinimapWindow::OnPaint(wxPaintEvent& event) { | |
| 343 | + | wxBufferedPaintDC dc(this); | |
| 344 | + | dc.SetBackground(*wxBLACK_BRUSH); | |
| 345 | + | dc.Clear(); | |
| 346 | + | ||
| 347 | + | if (!g_gui.IsEditorOpen()) return; | |
| 348 | + | ||
| 349 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 350 | + | MapCanvas* canvas = g_gui.GetCurrentMapTab()->GetCanvas(); | |
| 351 | + | ||
| 352 | + | int centerX, centerY; | |
| 353 | + | canvas->GetScreenCenter(¢erX, ¢erY); | |
| 354 | + | int floor = minimap_floor; | |
| 355 | + | ||
| 356 | + | // Store current state | |
| 357 | + | last_center_x = centerX; | |
| 358 | + | last_center_y = centerY; | |
| 359 | + | last_floor = floor; | |
| 360 | + | ||
| 361 | + | // Get window dimensions | |
| 362 | + | int windowWidth = wxPanel::GetSize().GetWidth(); | |
| 363 | + | int windowHeight = wxPanel::GetSize().GetHeight(); | |
| 364 | + | ||
| 365 | + | // Draw header with map info | |
| 366 | + | int headerHeight = 30; | |
| 367 | + | wxRect headerRect(0, 0, windowWidth, headerHeight); | |
| 368 | + | dc.SetBrush(wxBrush(wxColour(40, 40, 40))); | |
| 369 | + | dc.SetPen(*wxTRANSPARENT_PEN); | |
| 370 | + | dc.DrawRectangle(headerRect); | |
| 371 | + | ||
| 372 | + | // Draw map info text | |
| 373 | + | dc.SetTextForeground(wxColour(220, 220, 220)); | |
| 374 | + | wxFont font = dc.GetFont(); | |
| 375 | + | font.SetPointSize(9); | |
| 376 | + | dc.SetFont(font); | |
| 377 | + | ||
| 378 | + | wxString mapInfo = wxString::Format("Floor: %d | Position: %d,%d", | |
| 379 | + | floor, centerX, centerY); | |
| 380 | + | dc.DrawText(mapInfo, 10, 8); | |
| 381 | + | ||
| 382 | + | // Draw separator after position | |
| 383 | + | int textWidth, textHeight; | |
| 384 | + | dc.GetTextExtent(mapInfo, &textWidth, &textHeight); | |
| 385 | + | int sepX = 10 + textWidth + 10; | |
| 386 | + | dc.SetTextForeground(wxColour(120, 120, 120)); | |
| 387 | + | dc.DrawText("|", sepX, 8); | |
| 388 | + | int btnStartX = sepX + 15; | |
| 389 | + | ||
| 390 | + | // Draw header buttons in order: DOWN, UP, CACHE, then floor number | |
| 391 | + | int btnW = 28, btnH = 22, margin = 4; | |
| 392 | + | int y = (headerHeight - btnH) / 2; | |
| 393 | + | // Down button | |
| 394 | + | btn_down = wxRect(btnStartX, y, btnW, btnH); | |
| 395 | + | dc.SetBrush(wxBrush(wxColour(80, 80, 200))); | |
| 396 | + | dc.SetPen(*wxBLACK_PEN); | |
| 397 | + | dc.DrawRectangle(btn_down); | |
| 398 | + | dc.SetTextForeground(*wxWHITE); | |
| 399 | + | dc.DrawText("UP", btn_down.x + 7, btn_down.y - 2); | |
| 400 | + | // Up button | |
| 401 | + | btn_up = wxRect(btnStartX + btnW + margin, y, btnW, btnH); | |
| 402 | + | dc.SetBrush(wxBrush(wxColour(80, 80, 200))); | |
| 403 | + | dc.DrawRectangle(btn_up); | |
| 404 | + | dc.SetTextForeground(*wxWHITE); | |
| 405 | + | dc.DrawText("DOWN", btn_up.x + 7, btn_up.y - 2); | |
| 406 | + | // Cache button | |
| 407 | + | btn_cache = wxRect(btnStartX + 2 * (btnW + margin), y, btnW + 10, btnH); | |
| 408 | + | dc.SetBrush(wxBrush(wxColour(60, 180, 60))); | |
| 409 | + | dc.DrawRectangle(btn_cache); | |
| 410 | + | dc.SetTextForeground(*wxWHITE); | |
| 411 | + | dc.DrawText("Cache", btn_cache.x + 2, btn_cache.y + 2); | |
| 412 | + | // Floor number display | |
| 413 | + | wxString floorStr = wxString::Format("Floor: %d", minimap_floor); | |
| 414 | + | dc.SetTextForeground(wxColour(220, 220, 220)); | |
| 415 | + | int floorTextX = btn_cache.x + btn_cache.width + margin; | |
| 416 | + | dc.DrawText(floorStr, floorTextX, 8); | |
| 417 | + | ||
| 418 | + | // Draw separator before waypoint menu | |
| 419 | + | int floorTextWidth, floorTextHeight; | |
| 420 | + | dc.GetTextExtent(floorStr, &floorTextWidth, &floorTextHeight); | |
| 421 | + | int sep2X = floorTextX + floorTextWidth + 10; | |
| 422 | + | dc.SetTextForeground(wxColour(120, 120, 120)); | |
| 423 | + | dc.DrawText("|", sep2X, 8); | |
| 424 | + | ||
| 425 | + | // Minimap waypoint combo and add button | |
| 426 | + | int comboX = sep2X + 15; | |
| 427 | + | int comboY = y; | |
| 428 | + | int comboW = 120; | |
| 429 | + | int comboH = btnH; | |
| 430 | + | int addBtnW = 28; | |
| 431 | + | if (minimap_waypoint_combo && add_minimap_waypoint_btn) { | |
| 432 | + | minimap_waypoint_combo->SetSize(comboX, comboY, comboW, comboH); | |
| 433 | + | minimap_waypoint_combo->Show(); | |
| 434 | + | add_minimap_waypoint_btn->SetSize(comboX + comboW + margin, comboY, addBtnW, comboH); | |
| 435 | + | add_minimap_waypoint_btn->Show(); | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | // Draw separator after waypoint menu | |
| 439 | + | int sep3X = comboX + comboW + addBtnW + 2 * margin; | |
| 440 | + | dc.SetTextForeground(wxColour(120, 120, 120)); | |
| 441 | + | dc.DrawText("|", sep3X, 8); | |
| 442 | + | int saveBtnX = sep3X + 15; | |
| 443 | + | int saveBtnW = 48; | |
| 444 | + | int loadBtnW = 48; | |
| 445 | + | if (save_minimap_waypoints_btn && load_minimap_waypoints_btn) { | |
| 446 | + | save_minimap_waypoints_btn->SetSize(saveBtnX, comboY, saveBtnW, comboH); | |
| 447 | + | save_minimap_waypoints_btn->Show(); | |
| 448 | + | load_minimap_waypoints_btn->SetSize(saveBtnX + saveBtnW + margin, comboY, loadBtnW, comboH); | |
| 449 | + | load_minimap_waypoints_btn->Show(); | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | // In OnPaint, after the last separator, place the checkbox before Save/Load buttons | |
| 453 | + | int sep4X = sep3X + saveBtnW + loadBtnW + 2 * margin; | |
| 454 | + | dc.SetTextForeground(wxColour(120, 120, 120)); | |
| 455 | + | dc.DrawText("|", sep4X, 8); | |
| 456 | + | int checkboxX = sep4X + 15; | |
| 457 | + | if (save_cache_checkbox) { | |
| 458 | + | save_cache_checkbox->SetSize(checkboxX, comboY, 130, comboH); | |
| 459 | + | save_cache_checkbox->Show(); | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | // Draw minimap using cached blocks | |
| 463 | + | int padding = 10; | |
| 464 | + | int startX = std::max(0, centerX - (windowWidth / 2) - padding); | |
| 465 | + | int startY = std::max(0, centerY - ((windowHeight - headerHeight) / 2) - padding); | |
| 466 | + | int endX = std::min(editor.map.getWidth(), startX + windowWidth + padding * 2); | |
| 467 | + | int endY = std::min(editor.map.getHeight(), startY + (windowHeight - headerHeight) + padding * 2); | |
| 468 | + | int blockStartX = startX / BLOCK_SIZE; | |
| 469 | + | int blockEndX = (endX + BLOCK_SIZE - 1) / BLOCK_SIZE; | |
| 470 | + | int blockStartY = startY / BLOCK_SIZE; | |
| 471 | + | int blockEndY = (endY + BLOCK_SIZE - 1) / BLOCK_SIZE; | |
| 472 | + | for (int by = blockStartY; by < blockEndY; ++by) { | |
| 473 | + | for (int bx = blockStartX; bx < blockEndX; ++bx) { | |
| 474 | + | BlockKey key{bx, by, floor}; | |
| 475 | + | wxBitmap* bmp = nullptr; | |
| 476 | + | auto it = block_cache.find(key); | |
| 477 | + | if (it != block_cache.end()) { | |
| 478 | + | bmp = &it->second; | |
| 479 | + | } else if (IsBlockFilled(bx, by, floor)) { | |
| 480 | + | wxBitmap rendered = RenderBlock(bx, by, floor); | |
| 481 | + | block_cache[key] = rendered; | |
| 482 | + | bmp = &block_cache[key]; | |
| 483 | + | } | |
| 484 | + | if (bmp) { | |
| 485 | + | int drawX = bx * BLOCK_SIZE - startX; | |
| 486 | + | int drawY = by * BLOCK_SIZE - startY + headerHeight; | |
| 487 | + | dc.DrawBitmap(*bmp, drawX, drawY, false); | |
| 488 | + | } | |
| 489 | + | } | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | // Draw center marker | |
| 493 | + | dc.SetPen(wxPen(wxColour(255, 0, 0), 2)); | |
| 494 | + | int centerDrawX = windowWidth / 2; | |
| 495 | + | int centerDrawY = (windowHeight - headerHeight) / 2 + headerHeight; | |
| 496 | + | dc.DrawLine(centerDrawX - 5, centerDrawY, centerDrawX + 5, centerDrawY); | |
| 497 | + | dc.DrawLine(centerDrawX, centerDrawY - 5, centerDrawX, centerDrawY + 5); | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | void MinimapWindow::HandleHeaderButtonClick(const wxPoint& pt) { | |
| 501 | + | if (btn_cache.Contains(pt)) { | |
| 502 | + | StartCacheCurrentFloor(); | |
| 503 | + | } else if (btn_up.Contains(pt)) { | |
| 504 | + | minimap_floor = std::min(minimap_floor + 1, 15); // Clamp to max floor | |
| 505 | + | needs_update = true; | |
| 506 | + | Refresh(); | |
| 507 | + | } else if (btn_down.Contains(pt)) { | |
| 508 | + | minimap_floor = std::max(minimap_floor - 1, 0); // Clamp to min floor | |
| 509 | + | needs_update = true; | |
| 510 | + | Refresh(); | |
| 511 | + | } | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | void MinimapWindow::StartCacheCurrentFloor() { | |
| 515 | + | wxString msg = wxString::Format("Caching minimap floor %d...", minimap_floor); | |
| 516 | + | g_gui.CreateLoadBar(msg); | |
| 517 | + | CacheFilledBlocksForFloor(minimap_floor); | |
| 518 | + | if (save_cache_to_disk) { | |
| 519 | + | SaveBlockCacheToDisk(minimap_floor); | |
| 520 | + | } | |
| 521 | + | g_gui.DestroyLoadBar(); | |
| 522 | + | needs_update = true; | |
| 523 | + | Refresh(); | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | void MinimapWindow::BatchCacheFloor(int floor) { | |
| 527 | + | if (!g_gui.IsEditorOpen()) return; | |
| 528 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 529 | + | int mapWidth = editor.map.getWidth(); | |
| 530 | + | int mapHeight = editor.map.getHeight(); | |
| 531 | + | int totalRows = mapHeight; | |
| 532 | + | int doneRows = 0; | |
| 533 | + | ||
| 534 | + | // For each row, update the minimap cache and progress bar | |
| 535 | + | for (int y = 0; y < mapHeight; ++y) { | |
| 536 | + | for (int x = 0; x < mapWidth; ++x) { | |
| 537 | + | Tile* tile = editor.map.getTile(x, y, floor); | |
| 538 | + | // Optionally, update cache here if you have a block-based cache | |
| 539 | + | // For now, just access the tile to simulate caching | |
| 540 | + | (void)tile; | |
| 541 | + | } | |
| 542 | + | doneRows++; | |
| 543 | + | int percent = int((doneRows / (double)totalRows) * 100.0); | |
| 544 | + | g_gui.SetLoadDone(percent, wxString::Format("Caching row %d/%d", doneRows, totalRows)); | |
| 545 | + | wxYield(); // Keep UI responsive | |
| 546 | + | } | |
| 547 | + | needs_update = true; | |
| 548 | + | Refresh(); | |
| 549 | + | } | |
| 550 | + | ||
| 551 | + | void MinimapWindow::OnMouseClick(wxMouseEvent& event) { | |
| 552 | + | wxPoint pt(event.GetX(), event.GetY()); | |
| 553 | + | int headerHeight = 30; | |
| 554 | + | if (pt.y < headerHeight) { | |
| 555 | + | HandleHeaderButtonClick(pt); | |
| 556 | + | return; | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | if (!g_gui.IsEditorOpen()) | |
| 560 | + | return; | |
| 561 | + | ||
| 562 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 563 | + | MapCanvas* canvas = g_gui.GetCurrentMapTab()->GetCanvas(); | |
| 564 | + | ||
| 565 | + | int centerX, centerY; | |
| 566 | + | canvas->GetScreenCenter(¢erX, ¢erY); | |
| 567 | + | ||
| 568 | + | int windowWidth = GetSize().GetWidth(); | |
| 569 | + | int windowHeight = GetSize().GetHeight(); | |
| 570 | + | ||
| 571 | + | // Calculate the map position clicked | |
| 572 | + | int clickX = event.GetX(); | |
| 573 | + | int clickY = event.GetY() - headerHeight; // Adjust for header | |
| 574 | + | ||
| 575 | + | int mapX = centerX - (windowWidth / 2) + clickX; | |
| 576 | + | int mapY = centerY - ((windowHeight - headerHeight) / 2) + clickY; | |
| 577 | + | ||
| 578 | + | // Only process clicks below the header | |
| 579 | + | if (event.GetY() > headerHeight) { | |
| 580 | + | g_gui.SetScreenCenterPosition(Position(mapX, mapY, g_gui.GetCurrentFloor())); | |
| 581 | + | Refresh(); | |
| 582 | + | g_gui.RefreshView(); | |
| 583 | + | } | |
| 584 | + | } | |
| 585 | + | ||
| 586 | + | void MinimapWindow::OnKey(wxKeyEvent& event) { | |
| 587 | + | if (g_gui.GetCurrentTab() != nullptr) { | |
| 588 | + | g_gui.GetCurrentMapTab()->GetEventHandler()->AddPendingEvent(event); | |
| 589 | + | } | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | MinimapWindow::BlockPtr MinimapWindow::getBlock(int x, int y) { | |
| 593 | + | std::lock_guard<std::mutex> lock(m_mutex); | |
| 594 | + | uint32_t index = getBlockIndex(x, y); | |
| 595 | + | ||
| 596 | + | auto it = m_blocks.find(index); | |
| 597 | + | if (it == m_blocks.end()) { | |
| 598 | + | auto block = std::make_shared<MinimapBlock>(); | |
| 599 | + | m_blocks[index] = block; | |
| 600 | + | return block; | |
| 601 | + | } | |
| 602 | + | return it->second; | |
| 603 | + | } | |
| 604 | + | ||
| 605 | + | void MinimapWindow::updateBlock(BlockPtr block, int startX, int startY, int floor) { | |
| 606 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 607 | + | ||
| 608 | + | // Always update if the block's floor doesn't match current floor | |
| 609 | + | if (!block->needsUpdate && block->floor != floor) { | |
| 610 | + | block->needsUpdate = true; | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | if (!block->needsUpdate) return; | |
| 614 | + | ||
| 615 | + | wxBitmap bitmap(BLOCK_SIZE, BLOCK_SIZE); | |
| 616 | + | wxMemoryDC dc(bitmap); | |
| 617 | + | dc.SetBackground(*wxBLACK_BRUSH); | |
| 618 | + | dc.Clear(); | |
| 619 | + | ||
| 620 | + | // Store the floor this block was rendered for | |
| 621 | + | block->floor = floor; | |
| 622 | + | ||
| 623 | + | // Batch drawing by color like OTClient | |
| 624 | + | std::vector<std::vector<wxPoint>> colorPoints(256); | |
| 625 | + | ||
| 626 | + | for (int y = 0; y < BLOCK_SIZE; ++y) { | |
| 627 | + | for (int x = 0; x < BLOCK_SIZE; ++x) { | |
| 628 | + | int mapX = startX + x; | |
| 629 | + | int mapY = startY + y; | |
| 630 | + | ||
| 631 | + | Tile* tile = editor.map.getTile(mapX, mapY, floor); | |
| 632 | + | if (tile) { | |
| 633 | + | uint8_t color = tile->getMiniMapColor(); | |
| 634 | + | if (color != 255) { // Not transparent | |
| 635 | + | colorPoints[color].push_back(wxPoint(x, y)); | |
| 636 | + | } | |
| 637 | + | } | |
| 638 | + | } | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | // Draw all points of same color at once | |
| 642 | + | for (int color = 0; color < 256; ++color) { | |
| 643 | + | if (!colorPoints[color].empty()) { | |
| 644 | + | dc.SetPen(*pens[color]); | |
| 645 | + | for (const wxPoint& pt : colorPoints[color]) { | |
| 646 | + | dc.DrawPoint(pt); | |
| 647 | + | } | |
| 648 | + | } | |
| 649 | + | } | |
| 650 | + | ||
| 651 | + | block->bitmap = bitmap; | |
| 652 | + | block->needsUpdate = false; | |
| 653 | + | block->wasSeen = true; | |
| 654 | + | } | |
| 655 | + | ||
| 656 | + | void MinimapWindow::ClearCache() { | |
| 657 | + | // Simply refresh the window | |
| 658 | + | Refresh(); | |
| 659 | + | } | |
| 660 | + | ||
| 661 | + | void MinimapWindow::UpdateDrawnTiles(const PositionVector& positions) { | |
| 662 | + | // Just refresh the entire minimap - it's faster than checking which tiles need updating | |
| 663 | + | Refresh(); | |
| 664 | + | } | |
| 665 | + | ||
| 666 | + | void MinimapWindow::PreCacheEntireMap() { | |
| 667 | + | // No longer needed - we'll just draw the visible area on demand | |
| 668 | + | return; | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | void MinimapWindow::InitialLoad() { | |
| 672 | + | if (!g_gui.IsEditorOpen()) { | |
| 673 | + | return; | |
| 674 | + | } | |
| 675 | + | ||
| 676 | + | // Clear any existing cache | |
| 677 | + | std::lock_guard<std::mutex> lock(m_mutex); | |
| 678 | + | m_blocks.clear(); | |
| 679 | + | ||
| 680 | + | // Force an immediate refresh | |
| 681 | + | needs_update = true; | |
| 682 | + | Refresh(); | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | void MinimapWindow::UpdateMinimapWaypointCombo() { | |
| 686 | + | if (!minimap_waypoint_combo) return; | |
| 687 | + | minimap_waypoint_combo->Clear(); | |
| 688 | + | for (const auto& wp : minimap_waypoints) { | |
| 689 | + | minimap_waypoint_combo->Append(wp.name); | |
| 690 | + | } | |
| 691 | + | if (selected_minimap_waypoint_idx >= 0 && selected_minimap_waypoint_idx < (int)minimap_waypoints.size()) { | |
| 692 | + | minimap_waypoint_combo->SetSelection(selected_minimap_waypoint_idx); | |
| 693 | + | } | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | void MinimapWindow::OnMinimapWaypointSelected(wxCommandEvent& event) { | |
| 697 | + | int idx = minimap_waypoint_combo->GetSelection(); | |
| 698 | + | if (idx >= 0 && idx < (int)minimap_waypoints.size()) { | |
| 699 | + | TeleportToMinimapWaypoint(idx); | |
| 700 | + | } | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | void MinimapWindow::OnAddMinimapWaypoint(wxCommandEvent& event) { | |
| 704 | + | // Prompt for name | |
| 705 | + | wxString name = wxGetTextFromUser("Enter waypoint name:", "Add Minimap Waypoint"); | |
| 706 | + | if (name.IsEmpty()) return; | |
| 707 | + | // Use current minimap position | |
| 708 | + | int centerX = last_center_x; | |
| 709 | + | int centerY = last_center_y; | |
| 710 | + | int floor = minimap_floor; | |
| 711 | + | minimap_waypoints.emplace_back(name, Position(centerX, centerY, floor)); | |
| 712 | + | selected_minimap_waypoint_idx = minimap_waypoints.size() - 1; | |
| 713 | + | UpdateMinimapWaypointCombo(); | |
| 714 | + | } | |
| 715 | + | ||
| 716 | + | void MinimapWindow::TeleportToMinimapWaypoint(int idx) { | |
| 717 | + | if (idx < 0 || idx >= (int)minimap_waypoints.size()) return; | |
| 718 | + | const MinimapWaypoint& wp = minimap_waypoints[idx]; | |
| 719 | + | minimap_floor = wp.pos.z; | |
| 720 | + | needs_update = true; | |
| 721 | + | Refresh(); | |
| 722 | + | // Optionally, also move the main view: | |
| 723 | + | if (g_gui.IsEditorOpen()) { | |
| 724 | + | g_gui.GetCurrentMapTab()->SetScreenCenterPosition(wp.pos); | |
| 725 | + | } | |
| 726 | + | } | |
| 727 | + | ||
| 728 | + | void MinimapWindow::SaveMinimapWaypointsToXML() { | |
| 729 | + | wxString dataDir = g_gui.GetDataDirectory(); | |
| 730 | + | wxString filePath = dataDir + wxFileName::GetPathSeparator() + "minimap_waypoints.xml"; | |
| 731 | + | wxXmlDocument doc; | |
| 732 | + | wxXmlNode* root = new wxXmlNode(wxXML_ELEMENT_NODE, "minimap_waypoints"); | |
| 733 | + | for (const auto& wp : minimap_waypoints) { | |
| 734 | + | wxXmlNode* waypoint = new wxXmlNode(wxXML_ELEMENT_NODE, "waypoint"); | |
| 735 | + | waypoint->AddAttribute("name", wp.name); | |
| 736 | + | waypoint->AddAttribute("x", wxString::Format("%d", wp.pos.x)); | |
| 737 | + | waypoint->AddAttribute("y", wxString::Format("%d", wp.pos.y)); | |
| 738 | + | waypoint->AddAttribute("floor", wxString::Format("%d", wp.pos.z)); | |
| 739 | + | root->AddChild(waypoint); | |
| 740 | + | } | |
| 741 | + | doc.SetRoot(root); | |
| 742 | + | doc.Save(filePath); | |
| 743 | + | } | |
| 744 | + | ||
| 745 | + | void MinimapWindow::LoadMinimapWaypointsFromXML() { | |
| 746 | + | wxString dataDir = g_gui.GetDataDirectory(); | |
| 747 | + | wxString filePath = dataDir + wxFileName::GetPathSeparator() + "minimap_waypoints.xml"; | |
| 748 | + | minimap_waypoints.clear(); | |
| 749 | + | wxXmlDocument doc; | |
| 750 | + | if (doc.Load(filePath)) { | |
| 751 | + | wxXmlNode* root = doc.GetRoot(); | |
| 752 | + | if (root->GetName() == "minimap_waypoints") { | |
| 753 | + | wxXmlNode* waypoint = root->GetChildren(); | |
| 754 | + | while (waypoint) { | |
| 755 | + | if (waypoint->GetName() == "waypoint") { | |
| 756 | + | wxString name = waypoint->GetAttribute("name"); | |
| 757 | + | int x = 0, y = 0, floor = 0; | |
| 758 | + | waypoint->GetAttribute("x").ToInt(&x); | |
| 759 | + | waypoint->GetAttribute("y").ToInt(&y); | |
| 760 | + | waypoint->GetAttribute("floor").ToInt(&floor); | |
| 761 | + | minimap_waypoints.emplace_back(name, Position(x, y, floor)); | |
| 762 | + | } | |
| 763 | + | waypoint = waypoint->GetNext(); | |
| 764 | + | } | |
| 765 | + | selected_minimap_waypoint_idx = 0; | |
| 766 | + | UpdateMinimapWaypointCombo(); | |
| 767 | + | } | |
| 768 | + | } | |
| 769 | + | } | |
| 770 | + | ||
| 771 | + | void MinimapWindow::OnSaveMinimapWaypoints(wxCommandEvent& event) { | |
| 772 | + | SaveMinimapWaypointsToXML(); | |
| 773 | + | } | |
| 774 | + | ||
| 775 | + | void MinimapWindow::OnLoadMinimapWaypoints(wxCommandEvent& event) { | |
| 776 | + | LoadMinimapWaypointsFromXML(); | |
| 777 | + | } | |
| 778 | + | ||
| 779 | + | void MinimapWindow::CacheFilledBlocksForFloor(int floor) { | |
| 780 | + | if (!g_gui.IsEditorOpen()) return; | |
| 781 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 782 | + | int mapWidth = editor.map.getWidth(); | |
| 783 | + | int mapHeight = editor.map.getHeight(); | |
| 784 | + | int numBlocksX = (mapWidth + BLOCK_SIZE - 1) / BLOCK_SIZE; | |
| 785 | + | int numBlocksY = (mapHeight + BLOCK_SIZE - 1) / BLOCK_SIZE; | |
| 786 | + | int totalBlocks = numBlocksX * numBlocksY; | |
| 787 | + | int doneBlocks = 0; | |
| 788 | + | block_cache.clear(); | |
| 789 | + | for (int by = 0; by < numBlocksY; ++by) { | |
| 790 | + | for (int bx = 0; bx < numBlocksX; ++bx) { | |
| 791 | + | if (IsBlockFilled(bx, by, floor)) { | |
| 792 | + | wxBitmap bmp = RenderBlock(bx, by, floor); | |
| 793 | + | block_cache[{bx, by, floor}] = bmp; | |
| 794 | + | } | |
| 795 | + | doneBlocks++; | |
| 796 | + | int percent = int((doneBlocks / (double)totalBlocks) * 100.0); | |
| 797 | + | g_gui.SetLoadDone(percent, wxString::Format("Caching block %d/%d", doneBlocks, totalBlocks)); | |
| 798 | + | wxYield(); | |
| 799 | + | } | |
| 800 | + | } | |
| 801 | + | } | |
| 802 | + | ||
| 803 | + | void MinimapWindow::SaveBlockCacheToDisk(int floor) { | |
| 804 | + | wxString dataDir = g_gui.GetDataDirectory(); | |
| 805 | + | wxString mapName = GetCurrentMapName(); | |
| 806 | + | wxString cacheDir = dataDir + wxFileName::GetPathSeparator() + "cachedmaps" + wxFileName::GetPathSeparator() + mapName; | |
| 807 | + | wxFileName::Mkdir(cacheDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); | |
| 808 | + | for (const auto& pair : block_cache) { | |
| 809 | + | if (pair.first.z != floor) continue; | |
| 810 | + | wxString fileName = wxString::Format("block_%d_%d_%d.bin", pair.first.bx, pair.first.by, pair.first.z); | |
| 811 | + | wxString filePath = cacheDir + wxFileName::GetPathSeparator() + fileName; | |
| 812 | + | wxFFile file(filePath, "wb"); | |
| 813 | + | if (!file.IsOpened()) continue; | |
| 814 | + | wxImage img = pair.second.ConvertToImage(); | |
| 815 | + | for (int y = 0; y < BLOCK_SIZE; ++y) { | |
| 816 | + | for (int x = 0; x < BLOCK_SIZE; ++x) { | |
| 817 | + | unsigned char r = img.GetRed(x, y); | |
| 818 | + | unsigned char g = img.GetGreen(x, y); | |
| 819 | + | unsigned char b = img.GetBlue(x, y); | |
| 820 | + | // Find closest minimap color index | |
| 821 | + | uint8_t best = 0; | |
| 822 | + | int bestDist = 256*256*3; | |
| 823 | + | for (int i = 0; i < 256; ++i) { | |
| 824 | + | int dr = int(minimap_color[i].red) - r; | |
| 825 | + | int dg = int(minimap_color[i].green) - g; | |
| 826 | + | int db = int(minimap_color[i].blue) - b; | |
| 827 | + | int dist = dr*dr + dg*dg + db*db; | |
| 828 | + | if (dist < bestDist) { | |
| 829 | + | bestDist = dist; | |
| 830 | + | best = i; | |
| 831 | + | } | |
| 832 | + | } | |
| 833 | + | file.Write(&best, 1); | |
| 834 | + | } | |
| 835 | + | } | |
| 836 | + | file.Close(); | |
| 837 | + | } | |
| 838 | + | } | |
| 839 | + | ||
| 840 | + | void MinimapWindow::LoadBlockCacheFromDisk(int floor) { | |
| 841 | + | wxString dataDir = g_gui.GetDataDirectory(); | |
| 842 | + | wxString mapName = GetCurrentMapName(); | |
| 843 | + | wxString cacheDir = dataDir + wxFileName::GetPathSeparator() + "cachedmaps" + wxFileName::GetPathSeparator() + mapName; | |
| 844 | + | wxDir dir(cacheDir); | |
| 845 | + | wxString filename; | |
| 846 | + | bool cont = dir.GetFirst(&filename, wxString::Format("block_*_%d.bin", floor), wxDIR_FILES); | |
| 847 | + | while (cont) { | |
| 848 | + | wxFileName fn(cacheDir, filename); | |
| 849 | + | wxFFile file(fn.GetFullPath(), "rb"); | |
| 850 | + | if (!file.IsOpened()) { cont = dir.GetNext(&filename); continue; } | |
| 851 | + | std::vector<uint8_t> buffer(BLOCK_SIZE * BLOCK_SIZE); | |
| 852 | + | if (file.Read(buffer.data(), buffer.size()) == buffer.size()) { | |
| 853 | + | int bx = 0, by = 0, z = 0; | |
| 854 | + | if (sscanf(filename.mb_str(), "block_%d_%d_%d.bin", &bx, &by, &z) == 3) { | |
| 855 | + | wxImage img(BLOCK_SIZE, BLOCK_SIZE); | |
| 856 | + | for (int y = 0; y < BLOCK_SIZE; ++y) { | |
| 857 | + | for (int x = 0; x < BLOCK_SIZE; ++x) { | |
| 858 | + | uint8_t idx = buffer[y * BLOCK_SIZE + x]; | |
| 859 | + | img.SetRGB(x, y, minimap_color[idx].red, minimap_color[idx].green, minimap_color[idx].blue); | |
| 860 | + | } | |
| 861 | + | } | |
| 862 | + | block_cache[{bx, by, z}] = wxBitmap(img); | |
| 863 | + | } | |
| 864 | + | } | |
| 865 | + | file.Close(); | |
| 866 | + | cont = dir.GetNext(&filename); | |
| 867 | + | } | |
| 868 | + | } | |
| 869 | + | ||
| 870 | + | bool MinimapWindow::IsBlockFilled(int bx, int by, int floor) { | |
| 871 | + | if (!g_gui.IsEditorOpen()) return false; | |
| 872 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 873 | + | int startX = bx * BLOCK_SIZE; | |
| 874 | + | int startY = by * BLOCK_SIZE; | |
| 875 | + | for (int y = 0; y < BLOCK_SIZE; ++y) { | |
| 876 | + | for (int x = 0; x < BLOCK_SIZE; ++x) { | |
| 877 | + | Tile* tile = editor.map.getTile(startX + x, startY + y, floor); | |
| 878 | + | if (tile && tile->getMiniMapColor()) { | |
| 879 | + | return true; | |
| 880 | + | } | |
| 881 | + | } | |
| 882 | + | } | |
| 883 | + | return false; | |
| 884 | + | } | |
| 885 | + | ||
| 886 | + | wxBitmap MinimapWindow::RenderBlock(int bx, int by, int floor) { | |
| 887 | + | if (!g_gui.IsEditorOpen()) return wxBitmap(BLOCK_SIZE, BLOCK_SIZE); | |
| 888 | + | Editor& editor = *g_gui.GetCurrentEditor(); | |
| 889 | + | wxBitmap bitmap(BLOCK_SIZE, BLOCK_SIZE); | |
| 890 | + | wxMemoryDC dc(bitmap); | |
| 891 | + | dc.SetBackground(*wxBLACK_BRUSH); | |
| 892 | + | dc.Clear(); | |
| 893 | + | int startX = bx * BLOCK_SIZE; | |
| 894 | + | int startY = by * BLOCK_SIZE; | |
| 895 | + | for (int y = 0; y < BLOCK_SIZE; ++y) { | |
| 896 | + | for (int x = 0; x < BLOCK_SIZE; ++x) { | |
| 897 | + | Tile* tile = editor.map.getTile(startX + x, startY + y, floor); | |
| 898 | + | if (tile) { | |
| 899 | + | uint8_t color = tile->getMiniMapColor(); | |
| 900 | + | if (color) { | |
| 901 | + | dc.SetPen(*pens[color]); | |
| 902 | + | dc.DrawPoint(x, y); | |
| 903 | + | } | |
| 904 | + | } | |
| 905 | + | } | |
| 906 | + | } | |
| 907 | + | return bitmap; | |
| 908 | + | } | |
| 909 | + | ||
| 910 | + | wxString MinimapWindow::GetCurrentMapName() const { | |
| 911 | + | if (!g_gui.IsEditorOpen()) return "unnamed"; | |
| 912 | + | Editor* editor = g_gui.GetCurrentEditor(); | |
| 913 | + | if (!editor) return "unnamed"; | |
| 914 | + | wxString mapName = editor->map.getName(); | |
| 915 | + | if (mapName.IsEmpty()) mapName = "unnamed"; | |
| 916 | + | return mapName; | |
| 917 | + | } | |
| 918 | + | ||
| 919 | + | void MinimapWindow::SetMinimapFloor(int floor) { | |
| 920 | + | if (minimap_floor != floor) { | |
| 921 | + | minimap_floor = floor; | |
| 922 | + | needs_update = true; | |
| 923 | + | Refresh(); | |
| 924 | + | } | |
| 925 | + | } |
| @@ -0,0 +1,202 @@ | |||
| 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 | + | #ifndef RME_MINIMAP_WINDOW_H_ | |
| 19 | + | #define RME_MINIMAP_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "position.h" | |
| 22 | + | #include <wx/panel.h> | |
| 23 | + | #include <memory> | |
| 24 | + | #include <map> | |
| 25 | + | #include <thread> | |
| 26 | + | #include <mutex> | |
| 27 | + | #include <atomic> | |
| 28 | + | #include <wx/timer.h> | |
| 29 | + | #include <wx/pen.h> | |
| 30 | + | #include <vector> | |
| 31 | + | #include <wx/combobox.h> | |
| 32 | + | #include <wx/button.h> | |
| 33 | + | #include <wx/xml/xml.h> | |
| 34 | + | #include <wx/checkbox.h> | |
| 35 | + | ||
| 36 | + | class MinimapWindow : public wxPanel { | |
| 37 | + | public: | |
| 38 | + | enum { | |
| 39 | + | ID_MINIMAP_UPDATE = 45000, // Choose a number that won't conflict with other IDs | |
| 40 | + | ID_RESIZE_TIMER = 45001 // Timer ID for resize completion detection | |
| 41 | + | }; | |
| 42 | + | ||
| 43 | + | MinimapWindow(wxWindow* parent); | |
| 44 | + | virtual ~MinimapWindow(); | |
| 45 | + | ||
| 46 | + | void OnPaint(wxPaintEvent&); | |
| 47 | + | void OnEraseBackground(wxEraseEvent&) { } | |
| 48 | + | void OnMouseClick(wxMouseEvent&); | |
| 49 | + | void OnSize(wxSizeEvent&); | |
| 50 | + | void OnClose(wxCloseEvent&); | |
| 51 | + | void OnResizeTimer(wxTimerEvent&); | |
| 52 | + | ||
| 53 | + | void DelayedUpdate(); | |
| 54 | + | void OnDelayedUpdate(wxTimerEvent& event); | |
| 55 | + | void OnKey(wxKeyEvent& event); | |
| 56 | + | ||
| 57 | + | void ClearCache(); | |
| 58 | + | ||
| 59 | + | // Pre-cache method for building the entire minimap at load time | |
| 60 | + | void PreCacheEntireMap(); | |
| 61 | + | ||
| 62 | + | // Method to initialize minimap loading progressively on startup | |
| 63 | + | void InitialLoad(); | |
| 64 | + | ||
| 65 | + | void UpdateDrawnTiles(const PositionVector& positions); | |
| 66 | + | ||
| 67 | + | static const int BLOCK_SIZE = 256; // 256 IS most optimal 512 is too laggy and 64 is too small | |
| 68 | + | ||
| 69 | + | struct MinimapBlock { | |
| 70 | + | wxBitmap bitmap; | |
| 71 | + | bool needsUpdate = true; | |
| 72 | + | bool wasSeen = false; | |
| 73 | + | int floor = -1; | |
| 74 | + | ||
| 75 | + | MinimapBlock() : needsUpdate(true), wasSeen(false) {} | |
| 76 | + | }; | |
| 77 | + | ||
| 78 | + | using BlockPtr = std::shared_ptr<MinimapBlock>; | |
| 79 | + | using BlockMap = std::map<uint32_t, BlockPtr>; | |
| 80 | + | ||
| 81 | + | bool needs_update; | |
| 82 | + | ||
| 83 | + | void MarkBlockForUpdate(int x, int y) { | |
| 84 | + | if (auto block = getBlock(x, y)) { | |
| 85 | + | block->needsUpdate = true; | |
| 86 | + | } | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Minimap waypoint support | |
| 90 | + | struct MinimapWaypoint { | |
| 91 | + | wxString name; | |
| 92 | + | Position pos; | |
| 93 | + | MinimapWaypoint(const wxString& n, const Position& p) : name(n), pos(p) {} | |
| 94 | + | }; | |
| 95 | + | std::vector<MinimapWaypoint> minimap_waypoints; | |
| 96 | + | int selected_minimap_waypoint_idx = -1; | |
| 97 | + | wxComboBox* minimap_waypoint_combo = nullptr; | |
| 98 | + | wxButton* add_minimap_waypoint_btn = nullptr; | |
| 99 | + | ||
| 100 | + | void UpdateMinimapWaypointCombo(); | |
| 101 | + | void OnMinimapWaypointSelected(wxCommandEvent& event); | |
| 102 | + | void OnAddMinimapWaypoint(wxCommandEvent& event); | |
| 103 | + | void TeleportToMinimapWaypoint(int idx); | |
| 104 | + | ||
| 105 | + | // Save/Load buttons for minimap waypoints | |
| 106 | + | wxButton* save_minimap_waypoints_btn = nullptr; | |
| 107 | + | wxButton* load_minimap_waypoints_btn = nullptr; | |
| 108 | + | ||
| 109 | + | void SaveMinimapWaypointsToXML(); | |
| 110 | + | void LoadMinimapWaypointsFromXML(); | |
| 111 | + | void OnSaveMinimapWaypoints(wxCommandEvent& event); | |
| 112 | + | void OnLoadMinimapWaypoints(wxCommandEvent& event); | |
| 113 | + | void SetMinimapFloor(int floor); | |
| 114 | + | ||
| 115 | + | private: | |
| 116 | + | BlockMap m_blocks; | |
| 117 | + | std::mutex m_mutex; | |
| 118 | + | ||
| 119 | + | // Empty tile atlas for faster rendering | |
| 120 | + | wxBitmap empty_tile_atlas; | |
| 121 | + | bool empty_tile_atlas_initialized; | |
| 122 | + | ||
| 123 | + | // Minimap floor (separate from editor floor) | |
| 124 | + | int minimap_floor; | |
| 125 | + | ||
| 126 | + | // Button rectangles for header UI | |
| 127 | + | wxRect btn_cache; | |
| 128 | + | wxRect btn_up; | |
| 129 | + | wxRect btn_down; | |
| 130 | + | ||
| 131 | + | // Helper methods | |
| 132 | + | uint32_t getBlockIndex(int x, int y) const { | |
| 133 | + | return ((x / BLOCK_SIZE) * (65536 / BLOCK_SIZE)) + (y / BLOCK_SIZE); | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | wxPoint getBlockOffset(int x, int y) { | |
| 137 | + | return wxPoint(x - x % BLOCK_SIZE, y - y % BLOCK_SIZE); | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | BlockPtr getBlock(int x, int y); | |
| 141 | + | void updateBlock(BlockPtr block, int startX, int startY, int floor); | |
| 142 | + | ||
| 143 | + | wxBitmap buffer; | |
| 144 | + | std::mutex buffer_mutex; | |
| 145 | + | std::thread render_thread; | |
| 146 | + | std::atomic<bool> thread_running; | |
| 147 | + | ||
| 148 | + | // Window resizing handling | |
| 149 | + | bool is_resizing; | |
| 150 | + | wxTimer resize_timer; | |
| 151 | + | ||
| 152 | + | void RenderThreadFunction(); | |
| 153 | + | void StartRenderThread(); | |
| 154 | + | void StopRenderThread(); | |
| 155 | + | ||
| 156 | + | // Store last known state to detect changes | |
| 157 | + | int last_center_x; | |
| 158 | + | int last_center_y; | |
| 159 | + | int last_floor; | |
| 160 | + | ||
| 161 | + | wxPen* pens[256]; | |
| 162 | + | wxTimer update_timer; | |
| 163 | + | int last_start_x; | |
| 164 | + | int last_start_y; | |
| 165 | + | ||
| 166 | + | // Helper methods | |
| 167 | + | void InitializeEmptyTileAtlas(); | |
| 168 | + | void DrawEmptyTileAtlas(wxDC& dc, int startX, int startY, int width, int height); | |
| 169 | + | ||
| 170 | + | void DrawHeaderButtons(wxDC& dc, int windowWidth, int headerHeight); | |
| 171 | + | void HandleHeaderButtonClick(const wxPoint& pt); | |
| 172 | + | void StartCacheCurrentFloor(); | |
| 173 | + | void BatchCacheFloor(int floor); | |
| 174 | + | ||
| 175 | + | // Block-based cache | |
| 176 | + | struct BlockKey { | |
| 177 | + | int bx, by, z; | |
| 178 | + | bool operator<(const BlockKey& other) const { | |
| 179 | + | if (z != other.z) return z < other.z; | |
| 180 | + | if (bx != other.bx) return bx < other.bx; | |
| 181 | + | return by < other.by; | |
| 182 | + | } | |
| 183 | + | }; | |
| 184 | + | std::map<BlockKey, wxBitmap> block_cache; | |
| 185 | + | ||
| 186 | + | // UI: Save cache to disk checkbox | |
| 187 | + | wxCheckBox* save_cache_checkbox = nullptr; | |
| 188 | + | bool save_cache_to_disk = false; | |
| 189 | + | ||
| 190 | + | // Block cache logic | |
| 191 | + | void CacheFilledBlocksForFloor(int floor); | |
| 192 | + | void SaveBlockCacheToDisk(int floor); | |
| 193 | + | void LoadBlockCacheFromDisk(int floor); | |
| 194 | + | void ClearBlockCache(); | |
| 195 | + | bool IsBlockFilled(int bx, int by, int floor); | |
| 196 | + | wxBitmap RenderBlock(int bx, int by, int floor); | |
| 197 | + | wxString GetCurrentMapName() const; | |
| 198 | + | ||
| 199 | + | DECLARE_EVENT_TABLE() | |
| 200 | + | }; | |
| 201 | + | ||
| 202 | + | #endif |
| @@ -0,0 +1,18 @@ | |||
| 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" |
| @@ -0,0 +1,239 @@ | |||
| 1 | + | #include "monster_generator_dialog.h" | |
| 2 | + | #include "sprites.h" | |
| 3 | + | #include "outfit.h" | |
| 4 | + | #include "creatures.h" | |
| 5 | + | #include "pugixml.h" | |
| 6 | + | ||
| 7 | + | BEGIN_EVENT_TABLE(MonsterGeneratorDialog, wxDialog) | |
| 8 | + | EVT_BUTTON(wxID_SAVE, MonsterGeneratorDialog::OnSave) | |
| 9 | + | EVT_BUTTON(wxID_OPEN, MonsterGeneratorDialog::OnLoad) | |
| 10 | + | EVT_SPINCTRL(SPIN_OUTFIT_TYPE, MonsterGeneratorDialog::OnOutfitChange) | |
| 11 | + | EVT_BUTTON(BUTTON_ADD_ATTACK, MonsterGeneratorDialog::OnAddAttack) | |
| 12 | + | EVT_BUTTON(BUTTON_ADD_DEFENSE, MonsterGeneratorDialog::OnAddDefense) | |
| 13 | + | EVT_BUTTON(BUTTON_ADD_LOOT, MonsterGeneratorDialog::OnAddLoot) | |
| 14 | + | END_EVENT_TABLE() | |
| 15 | + | ||
| 16 | + | MonsterGeneratorDialog::MonsterGeneratorDialog(wxWindow* parent) | |
| 17 | + | : wxDialog(parent, wxID_ANY, "Monster Generator", wxDefaultPosition, wxSize(800, 600)) | |
| 18 | + | { | |
| 19 | + | CreateGUI(); | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | MonsterGeneratorDialog::~MonsterGeneratorDialog() | |
| 23 | + | { | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | void MonsterGeneratorDialog::CreateGUI() | |
| 27 | + | { | |
| 28 | + | // Create main sizer | |
| 29 | + | wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); | |
| 30 | + | ||
| 31 | + | // Create notebook | |
| 32 | + | notebook = new wxNotebook(this, wxID_ANY); | |
| 33 | + | ||
| 34 | + | // Basic Info Page | |
| 35 | + | basicPanel = new wxPanel(notebook); | |
| 36 | + | wxBoxSizer* basicSizer = new wxBoxSizer(wxVERTICAL); | |
| 37 | + | ||
| 38 | + | // Name | |
| 39 | + | wxBoxSizer* nameSizer = new wxBoxSizer(wxHORIZONTAL); | |
| 40 | + | nameSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Name:"), 0, wxALL, 5); | |
| 41 | + | nameCtrl = new wxTextCtrl(basicPanel, wxID_ANY); | |
| 42 | + | nameSizer->Add(nameCtrl, 1, wxEXPAND | wxALL, 5); | |
| 43 | + | basicSizer->Add(nameSizer, 0, wxEXPAND); | |
| 44 | + | ||
| 45 | + | // Description | |
| 46 | + | wxBoxSizer* descSizer = new wxBoxSizer(wxHORIZONTAL); | |
| 47 | + | descSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Description:"), 0, wxALL, 5); | |
| 48 | + | descriptionCtrl = new wxTextCtrl(basicPanel, wxID_ANY); | |
| 49 | + | descSizer->Add(descriptionCtrl, 1, wxEXPAND | wxALL, 5); | |
| 50 | + | basicSizer->Add(descSizer, 0, wxEXPAND); | |
| 51 | + | ||
| 52 | + | // Race | |
| 53 | + | wxBoxSizer* raceSizer = new wxBoxSizer(wxHORIZONTAL); | |
| 54 | + | raceSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Race:"), 0, wxALL, 5); | |
| 55 | + | wxArrayString races; | |
| 56 | + | races.Add("blood"); | |
| 57 | + | races.Add("venom"); | |
| 58 | + | races.Add("undead"); | |
| 59 | + | races.Add("fire"); | |
| 60 | + | races.Add("energy"); | |
| 61 | + | raceChoice = new wxChoice(basicPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, races); | |
| 62 | + | raceSizer->Add(raceChoice, 1, wxEXPAND | wxALL, 5); | |
| 63 | + | basicSizer->Add(raceSizer, 0, wxEXPAND); | |
| 64 | + | ||
| 65 | + | // Stats | |
| 66 | + | wxFlexGridSizer* statsSizer = new wxFlexGridSizer(2, 5, 5); | |
| 67 | + | ||
| 68 | + | statsSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Experience:")); | |
| 69 | + | experienceCtrl = new wxSpinCtrl(basicPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 999999999); | |
| 70 | + | statsSizer->Add(experienceCtrl); | |
| 71 | + | ||
| 72 | + | statsSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Health:")); | |
| 73 | + | healthCtrl = new wxSpinCtrl(basicPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999999999); | |
| 74 | + | statsSizer->Add(healthCtrl); | |
| 75 | + | ||
| 76 | + | statsSizer->Add(new wxStaticText(basicPanel, wxID_ANY, "Speed:")); | |
| 77 | + | speedCtrl = new wxSpinCtrl(basicPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 9999); | |
| 78 | + | statsSizer->Add(speedCtrl); | |
| 79 | + | ||
| 80 | + | basicSizer->Add(statsSizer, 0, wxALL, 5); | |
| 81 | + | basicPanel->SetSizer(basicSizer); | |
| 82 | + | ||
| 83 | + | // Outfit Page | |
| 84 | + | outfitPanel = new wxPanel(notebook); | |
| 85 | + | wxBoxSizer* outfitSizer = new wxBoxSizer(wxVERTICAL); | |
| 86 | + | ||
| 87 | + | wxFlexGridSizer* outfitGridSizer = new wxFlexGridSizer(2, 5, 5); | |
| 88 | + | ||
| 89 | + | outfitGridSizer->Add(new wxStaticText(outfitPanel, wxID_ANY, "Look Type:")); | |
| 90 | + | lookTypeCtrl = new wxSpinCtrl(outfitPanel, SPIN_OUTFIT_TYPE, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 999); | |
| 91 | + | outfitGridSizer->Add(lookTypeCtrl); | |
| 92 | + | ||
| 93 | + | outfitGridSizer->Add(new wxStaticText(outfitPanel, wxID_ANY, "Head:")); | |
| 94 | + | headCtrl = new wxSpinCtrl(outfitPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255); | |
| 95 | + | outfitGridSizer->Add(headCtrl); | |
| 96 | + | ||
| 97 | + | outfitGridSizer->Add(new wxStaticText(outfitPanel, wxID_ANY, "Body:")); | |
| 98 | + | bodyCtrl = new wxSpinCtrl(outfitPanel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255); | |
| 99 | + | outfitGridSizer->Add(bodyCtrl); | |
| 100 | + | ||
| 101 | + | outfitSizer->Add(outfitGridSizer, 0, wxALL, 5); | |
| 102 | + | ||
| 103 | + | // Outfit preview | |
| 104 | + | outfitPreview = new wxStaticBitmap(outfitPanel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(64, 64)); | |
| 105 | + | outfitSizer->Add(outfitPreview, 0, wxALL | wxCENTER, 5); | |
| 106 | + | ||
| 107 | + | outfitPanel->SetSizer(outfitSizer); | |
| 108 | + | ||
| 109 | + | // Add pages to notebook | |
| 110 | + | notebook->AddPage(basicPanel, "Basic Info"); | |
| 111 | + | notebook->AddPage(outfitPanel, "Outfit"); | |
| 112 | + | ||
| 113 | + | // Add notebook to main sizer | |
| 114 | + | mainSizer->Add(notebook, 1, wxEXPAND | wxALL, 5); | |
| 115 | + | ||
| 116 | + | // Buttons | |
| 117 | + | wxBoxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL); | |
| 118 | + | buttonSizer->Add(new wxButton(this, wxID_SAVE, "Save"), 0, wxALL, 5); | |
| 119 | + | buttonSizer->Add(new wxButton(this, wxID_OPEN, "Load"), 0, wxALL, 5); | |
| 120 | + | buttonSizer->Add(new wxButton(this, wxID_CANCEL, "Close"), 0, wxALL, 5); | |
| 121 | + | mainSizer->Add(buttonSizer, 0, wxALIGN_RIGHT | wxALL, 5); | |
| 122 | + | ||
| 123 | + | SetSizer(mainSizer); | |
| 124 | + | Layout(); | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | void MonsterGeneratorDialog::UpdatePreview() | |
| 128 | + | { | |
| 129 | + | // Get the outfit values | |
| 130 | + | int type = lookTypeCtrl->GetValue(); | |
| 131 | + | int head = headCtrl->GetValue(); | |
| 132 | + | int body = bodyCtrl->GetValue(); | |
| 133 | + | int legs = legsCtrl->GetValue(); | |
| 134 | + | int feet = feetCtrl->GetValue(); | |
| 135 | + | int addons = addonsCtrl->GetValue(); | |
| 136 | + | ||
| 137 | + | // Create outfit object | |
| 138 | + | Outfit outfit; | |
| 139 | + | outfit.lookType = type; | |
| 140 | + | outfit.lookHead = head; | |
| 141 | + | outfit.lookBody = body; | |
| 142 | + | outfit.lookLegs = legs; | |
| 143 | + | outfit.lookFeet = feet; | |
| 144 | + | outfit.lookAddons = addons; | |
| 145 | + | ||
| 146 | + | // Get the sprite and convert to wxBitmap | |
| 147 | + | if(g_sprites.GetOutfitSprite(outfit)) { | |
| 148 | + | wxBitmap bmp = wxBitmap(g_sprites.GetOutfitSprite(outfit)->getImage()); | |
| 149 | + | outfitPreview->SetBitmap(bmp); | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | void MonsterGeneratorDialog::OnOutfitChange(wxSpinEvent& event) | |
| 154 | + | { | |
| 155 | + | UpdatePreview(); | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | void MonsterGeneratorDialog::SaveMonsterFile() | |
| 159 | + | { | |
| 160 | + | wxFileDialog saveFileDialog(this, "Save Monster File", "", "", | |
| 161 | + | "XML files (*.xml)|*.xml", wxFD_SAVE|wxFD_OVERWRITE_PROMPT); | |
| 162 | + | if (saveFileDialog.ShowModal() == wxID_CANCEL) | |
| 163 | + | return; | |
| 164 | + | ||
| 165 | + | pugi::xml_document doc; | |
| 166 | + | pugi::xml_node monster = doc.append_child("monster"); | |
| 167 | + | ||
| 168 | + | // Basic attributes | |
| 169 | + | monster.append_attribute("name") = nameCtrl->GetValue(); | |
| 170 | + | monster.append_attribute("nameDescription") = descriptionCtrl->GetValue(); | |
| 171 | + | monster.append_attribute("race") = raceChoice->GetStringSelection(); | |
| 172 | + | monster.append_attribute("experience") = experienceCtrl->GetValue(); | |
| 173 | + | monster.append_attribute("speed") = speedCtrl->GetValue(); | |
| 174 | + | ||
| 175 | + | // Health | |
| 176 | + | pugi::xml_node health = monster.append_child("health"); | |
| 177 | + | health.append_attribute("now") = healthCtrl->GetValue(); | |
| 178 | + | health.append_attribute("max") = healthCtrl->GetValue(); | |
| 179 | + | ||
| 180 | + | // Look | |
| 181 | + | pugi::xml_node look = monster.append_child("look"); | |
| 182 | + | look.append_attribute("type") = lookTypeCtrl->GetValue(); | |
| 183 | + | look.append_attribute("head") = headCtrl->GetValue(); | |
| 184 | + | look.append_attribute("body") = bodyCtrl->GetValue(); | |
| 185 | + | look.append_attribute("legs") = legsCtrl->GetValue(); | |
| 186 | + | look.append_attribute("feet") = feetCtrl->GetValue(); | |
| 187 | + | look.append_attribute("addons") = addonsCtrl->GetValue(); | |
| 188 | + | ||
| 189 | + | // Save the file | |
| 190 | + | doc.save_file(saveFileDialog.GetPath().mb_str()); | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | void MonsterGeneratorDialog::OnSave(wxCommandEvent& event) | |
| 194 | + | { | |
| 195 | + | SaveMonsterFile(); | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | void MonsterGeneratorDialog::OnLoad(wxCommandEvent& event) | |
| 199 | + | { | |
| 200 | + | wxFileDialog openFileDialog(this, "Open Monster File", "", "", | |
| 201 | + | "XML files (*.xml)|*.xml", wxFD_OPEN|wxFD_FILE_MUST_EXIST); | |
| 202 | + | if (openFileDialog.ShowModal() == wxID_CANCEL) | |
| 203 | + | return; | |
| 204 | + | ||
| 205 | + | pugi::xml_document doc; | |
| 206 | + | if(!doc.load_file(openFileDialog.GetPath().mb_str())) { | |
| 207 | + | wxMessageBox("Failed to load monster file!", "Error", wxICON_ERROR); | |
| 208 | + | return; | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | pugi::xml_node monster = doc.child("monster"); | |
| 212 | + | if(!monster) { | |
| 213 | + | wxMessageBox("Invalid monster file format!", "Error", wxICON_ERROR); | |
| 214 | + | return; | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // Load basic attributes | |
| 218 | + | nameCtrl->SetValue(monster.attribute("name").as_string()); | |
| 219 | + | descriptionCtrl->SetValue(monster.attribute("nameDescription").as_string()); | |
| 220 | + | raceChoice->SetStringSelection(monster.attribute("race").as_string()); | |
| 221 | + | experienceCtrl->SetValue(monster.attribute("experience").as_int()); | |
| 222 | + | speedCtrl->SetValue(monster.attribute("speed").as_int()); | |
| 223 | + | ||
| 224 | + | // Load health | |
| 225 | + | if(pugi::xml_node health = monster.child("health")) { | |
| 226 | + | healthCtrl->SetValue(health.attribute("max").as_int()); | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // Load look | |
| 230 | + | if(pugi::xml_node look = monster.child("look")) { | |
| 231 | + | lookTypeCtrl->SetValue(look.attribute("type").as_int()); | |
| 232 | + | headCtrl->SetValue(look.attribute("head").as_int()); | |
| 233 | + | bodyCtrl->SetValue(look.attribute("body").as_int()); | |
| 234 | + | legsCtrl->SetValue(look.attribute("legs").as_int()); | |
| 235 | + | feetCtrl->SetValue(look.attribute("feet").as_int()); | |
| 236 | + | addonsCtrl->SetValue(look.attribute("addons").as_int()); | |
| 237 | + | UpdatePreview(); | |
| 238 | + | } | |
| 239 | + | } |