| @@ -0,0 +1,10 @@ | |||
| 1 | + | @echo off | |
| 2 | + | setlocal | |
| 3 | + | cd /d "%~dp0" | |
| 4 | + | if not exist "RD_Map_Editor.exe" ( | |
| 5 | + | echo ERROR: RD_Map_Editor.exe was not found beside this launcher. | |
| 6 | + | pause | |
| 7 | + | exit /b 1 | |
| 8 | + | ) | |
| 9 | + | start "" "RD_Map_Editor.exe" -force-multi-instance | |
| 10 | + | exit /b 0 |
| @@ -0,0 +1,9 @@ | |||
| 1 | + | // server.json example | |
| 2 | + | { | |
| 3 | + | "map": "mymap.otbm", | |
| 4 | + | "port": 3333, | |
| 5 | + | "password": "secret", | |
| 6 | + | "max_clients": 16, | |
| 7 | + | "auto_save": true, | |
| 8 | + | "save_interval": 300 | |
| 9 | + | } |
| @@ -0,0 +1,887 @@ | |||
| 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 | + | ||
| 22 | + | #include "about_window.h" | |
| 23 | + | #include <fstream> | |
| 24 | + | #include <typeinfo> | |
| 25 | + | #include <memory> | |
| 26 | + | ||
| 27 | + | class GamePanel : public wxPanel { | |
| 28 | + | public: | |
| 29 | + | GamePanel(wxWindow* parent, int width, int height); | |
| 30 | + | virtual ~GamePanel(); | |
| 31 | + | ||
| 32 | + | void OnPaint(wxPaintEvent&); | |
| 33 | + | void OnKeyDown(wxKeyEvent&); | |
| 34 | + | void OnKeyUp(wxKeyEvent&); | |
| 35 | + | void OnIdle(wxIdleEvent&); | |
| 36 | + | ||
| 37 | + | void pause() { | |
| 38 | + | paused_val = true; | |
| 39 | + | } | |
| 40 | + | void unpause() { | |
| 41 | + | paused_val = false; | |
| 42 | + | } | |
| 43 | + | bool paused() const { | |
| 44 | + | return paused_val || dead; | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | protected: | |
| 48 | + | virtual void Render(wxDC& pdc) = 0; | |
| 49 | + | virtual void GameLoop(int time) = 0; | |
| 50 | + | virtual void OnKey(wxKeyEvent& event, bool down) = 0; | |
| 51 | + | ||
| 52 | + | virtual int getFPS() const = 0; | |
| 53 | + | ||
| 54 | + | protected: | |
| 55 | + | wxStopWatch game_timer; | |
| 56 | + | ||
| 57 | + | private: | |
| 58 | + | bool paused_val; | |
| 59 | + | ||
| 60 | + | DECLARE_EVENT_TABLE() | |
| 61 | + | ||
| 62 | + | protected: | |
| 63 | + | bool dead; | |
| 64 | + | }; | |
| 65 | + | ||
| 66 | + | const int TETRIS_MAPHEIGHT = 20; | |
| 67 | + | const int TETRIS_MAPWIDTH = 10; | |
| 68 | + | ||
| 69 | + | class TetrisPanel : public GamePanel { | |
| 70 | + | public: | |
| 71 | + | TetrisPanel(wxWindow* parent); | |
| 72 | + | ~TetrisPanel(); | |
| 73 | + | ||
| 74 | + | protected: | |
| 75 | + | virtual void Render(wxDC& pdc); | |
| 76 | + | virtual void GameLoop(int time); | |
| 77 | + | virtual void OnKey(wxKeyEvent& event, bool down); | |
| 78 | + | ||
| 79 | + | virtual int getFPS() const { | |
| 80 | + | return lines / 10 + 3; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | enum Color { | |
| 84 | + | NO_COLOR, | |
| 85 | + | RED, | |
| 86 | + | BLUE, | |
| 87 | + | GREEN, | |
| 88 | + | STEEL, | |
| 89 | + | YELLOW, | |
| 90 | + | PURPLE, | |
| 91 | + | WHITE, | |
| 92 | + | }; | |
| 93 | + | ||
| 94 | + | enum BlockType { | |
| 95 | + | FIRST_BLOCK, | |
| 96 | + | BLOCK_TOWER = FIRST_BLOCK, | |
| 97 | + | BLOCK_SQUARE, | |
| 98 | + | BLOCK_TRIANGLE, | |
| 99 | + | BLOCK_L, | |
| 100 | + | BLOCK_J, | |
| 101 | + | BLOCK_Z, | |
| 102 | + | BLOCK_S, | |
| 103 | + | LAST_BLOCK = BLOCK_S | |
| 104 | + | }; | |
| 105 | + | ||
| 106 | + | struct Block { | |
| 107 | + | Color structure[4][4]; | |
| 108 | + | int x, y; | |
| 109 | + | } block; | |
| 110 | + | ||
| 111 | + | const wxBrush& GetBrush(Color color) const; | |
| 112 | + | bool BlockCollisionTest(int mx, int my) const; | |
| 113 | + | void RemoveRow(int row); | |
| 114 | + | void NewBlock(); | |
| 115 | + | void MoveBlock(int x, int y); | |
| 116 | + | void RotateBlock(); | |
| 117 | + | void NewGame(); | |
| 118 | + | void EndGame(); | |
| 119 | + | void AddScore(int lines); | |
| 120 | + | ||
| 121 | + | int score; | |
| 122 | + | int lines; | |
| 123 | + | Color map[TETRIS_MAPWIDTH][TETRIS_MAPHEIGHT]; | |
| 124 | + | }; | |
| 125 | + | ||
| 126 | + | const int SNAKE_MAPHEIGHT = 20; | |
| 127 | + | const int SNAKE_MAPWIDTH = 20; | |
| 128 | + | ||
| 129 | + | class SnakePanel : public GamePanel { | |
| 130 | + | public: | |
| 131 | + | SnakePanel(wxWindow* parent); | |
| 132 | + | ~SnakePanel(); | |
| 133 | + | ||
| 134 | + | protected: | |
| 135 | + | virtual void Render(wxDC& pdc); | |
| 136 | + | virtual void GameLoop(int time); | |
| 137 | + | virtual void OnKey(wxKeyEvent& event, bool down); | |
| 138 | + | ||
| 139 | + | virtual int getFPS() const { | |
| 140 | + | return 7; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | enum { | |
| 144 | + | NORTH, | |
| 145 | + | SOUTH, | |
| 146 | + | WEST, | |
| 147 | + | EAST, | |
| 148 | + | }; | |
| 149 | + | ||
| 150 | + | void NewApple(); | |
| 151 | + | void Move(int dir); | |
| 152 | + | void NewGame(); | |
| 153 | + | void EndGame(); | |
| 154 | + | void UpdateTitle(); | |
| 155 | + | ||
| 156 | + | // -1 is apple, 0 is nothing, >0 is snake (will decay in n rounds) | |
| 157 | + | int length; | |
| 158 | + | int last_dir; | |
| 159 | + | int map[SNAKE_MAPWIDTH][SNAKE_MAPHEIGHT]; | |
| 160 | + | }; | |
| 161 | + | ||
| 162 | + | //============================================================================= | |
| 163 | + | // About Window - Information window about the application | |
| 164 | + | ||
| 165 | + | BEGIN_EVENT_TABLE(AboutWindow, wxDialog) | |
| 166 | + | EVT_BUTTON(wxID_OK, AboutWindow::OnClickOK) | |
| 167 | + | EVT_BUTTON(ABOUT_VIEW_LICENSE, AboutWindow::OnClickLicense) | |
| 168 | + | EVT_MENU(ABOUT_RUN_TETRIS, AboutWindow::OnTetris) | |
| 169 | + | EVT_MENU(ABOUT_RUN_SNAKE, AboutWindow::OnSnake) | |
| 170 | + | EVT_MENU(wxID_CANCEL, AboutWindow::OnClickOK) | |
| 171 | + | END_EVENT_TABLE() | |
| 172 | + | ||
| 173 | + | AboutWindow::AboutWindow(wxWindow* parent) : | |
| 174 | + | wxDialog(parent, wxID_ANY, "About", wxDefaultPosition, wxSize(300, 320), wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX), | |
| 175 | + | game_panel(nullptr) { | |
| 176 | + | wxString about; | |
| 177 | + | ||
| 178 | + | about << "RD Map Editor (OTARME RD)\n"; | |
| 179 | + | about << "(based on OTA Remere's Map Editor)\n\n"; | |
| 180 | + | about << "This program is a map editor for game servers\nthat derivied from OpenTibia project.\n\n"; | |
| 181 | + | about << "RD edition\n\n"; | |
| 182 | + | ||
| 183 | + | about << "Version " << __W_RME_VERSION__ << " for "; | |
| 184 | + | about << | |
| 185 | + | #ifdef __WINDOWS__ | |
| 186 | + | "Windows"; | |
| 187 | + | #elif __LINUX__ | |
| 188 | + | "Linux"; | |
| 189 | + | #elif __APPLE__ | |
| 190 | + | "macOS"; | |
| 191 | + | #else | |
| 192 | + | "other OS"; | |
| 193 | + | #endif | |
| 194 | + | about << "\n\n"; | |
| 195 | + | ||
| 196 | + | about << "Using " << wxVERSION_STRING << " interface\n"; | |
| 197 | + | about << "OpenGL version " << wxString((char*)glGetString(GL_VERSION), wxConvUTF8) << "\n"; | |
| 198 | + | about << "\n"; | |
| 199 | + | about << "This program comes with ABSOLUTELY NO WARRANTY;\n"; | |
| 200 | + | about << "for details see the LICENSE file.\n"; | |
| 201 | + | about << "This is free software, and you are welcome to redistribute it\n"; | |
| 202 | + | about << "under certain conditions.\n"; | |
| 203 | + | about << "\n"; | |
| 204 | + | about << "Compiled on: " << __TDATE__ << " : " << __TTIME__ << "\n"; | |
| 205 | + | about << "Compiled with: " << BOOST_COMPILER << "\n"; | |
| 206 | + | ||
| 207 | + | topsizer = newd wxBoxSizer(wxVERTICAL); | |
| 208 | + | ||
| 209 | + | topsizer->Add(newd wxStaticText(this, wxID_ANY, about), 1, wxALL, 20); | |
| 210 | + | ||
| 211 | + | wxSizer* choicesizer = newd wxBoxSizer(wxHORIZONTAL); | |
| 212 | + | choicesizer->Add(newd wxButton(this, wxID_OK, "OK"), wxSizerFlags(1).Center()); | |
| 213 | + | topsizer->Add(choicesizer, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT | wxBOTTOM, 20); | |
| 214 | + | ||
| 215 | + | wxAcceleratorEntry entries[3]; | |
| 216 | + | entries[0].Set(wxACCEL_NORMAL, WXK_ESCAPE, wxID_CANCEL); | |
| 217 | + | entries[1].Set(wxACCEL_NORMAL, 't', ABOUT_RUN_TETRIS); | |
| 218 | + | entries[2].Set(wxACCEL_NORMAL, 's', ABOUT_RUN_SNAKE); | |
| 219 | + | wxAcceleratorTable accel(3, entries); | |
| 220 | + | SetAcceleratorTable(accel); | |
| 221 | + | ||
| 222 | + | SetSizerAndFit(topsizer); | |
| 223 | + | Centre(wxBOTH); | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | AboutWindow::~AboutWindow() { | |
| 227 | + | //// | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | void AboutWindow::OnClickOK(wxCommandEvent& WXUNUSED(event)) { | |
| 231 | + | EndModal(0); | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | void AboutWindow::OnClickLicense(wxCommandEvent& WXUNUSED(event)) { | |
| 235 | + | FileName path; | |
| 236 | + | try { | |
| 237 | + | path = wxStandardPaths::Get().GetExecutablePath(); | |
| 238 | + | } catch (std::bad_cast&) { | |
| 239 | + | return; | |
| 240 | + | } | |
| 241 | + | path.SetFullName("COPYING.txt"); | |
| 242 | + | std::ifstream gpl(path.GetFullPath().mb_str()); | |
| 243 | + | ||
| 244 | + | std::string gpl_str; | |
| 245 | + | char ch; | |
| 246 | + | while (gpl.get(ch)) { | |
| 247 | + | gpl_str += ch; | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | g_gui.ShowTextBox(this, "License", wxstr(gpl_str.size() ? gpl_str : "The COPYING.txt file is not available.")); | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | void AboutWindow::OnTetris(wxCommandEvent&) { | |
| 254 | + | if (!game_panel) { | |
| 255 | + | DestroyChildren(); | |
| 256 | + | game_panel = newd TetrisPanel(this); | |
| 257 | + | topsizer->Add(game_panel, 1, wxALIGN_CENTER | wxALL, 7); | |
| 258 | + | Fit(); | |
| 259 | + | game_panel->SetFocus(); | |
| 260 | + | SetWindowStyleFlag(wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX); | |
| 261 | + | Refresh(); | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | void AboutWindow::OnSnake(wxCommandEvent&) { | |
| 266 | + | if (!game_panel) { | |
| 267 | + | DestroyChildren(); | |
| 268 | + | game_panel = newd SnakePanel(this); | |
| 269 | + | topsizer->Add(game_panel, 1, wxALIGN_CENTER | wxALL, 7); | |
| 270 | + | Fit(); | |
| 271 | + | game_panel->SetFocus(); | |
| 272 | + | SetWindowStyleFlag(wxRESIZE_BORDER | wxCAPTION | wxCLOSE_BOX); | |
| 273 | + | Refresh(); | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | //============================================================================= | |
| 278 | + | // GamePanel - Abstract class for games | |
| 279 | + | ||
| 280 | + | BEGIN_EVENT_TABLE(GamePanel, wxPanel) | |
| 281 | + | EVT_KEY_DOWN(GamePanel::OnKeyDown) | |
| 282 | + | EVT_KEY_UP(GamePanel::OnKeyUp) | |
| 283 | + | EVT_PAINT(GamePanel::OnPaint) | |
| 284 | + | EVT_IDLE(GamePanel::OnIdle) | |
| 285 | + | END_EVENT_TABLE() | |
| 286 | + | ||
| 287 | + | GamePanel::GamePanel(wxWindow* parent, int width, int height) : | |
| 288 | + | wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(width, height), wxWANTS_CHARS), | |
| 289 | + | paused_val(false), | |
| 290 | + | dead(false) { | |
| 291 | + | // Receive idle events | |
| 292 | + | SetExtraStyle(wxWS_EX_PROCESS_IDLE); | |
| 293 | + | // Complete redraw | |
| 294 | + | SetBackgroundStyle(wxBG_STYLE_CUSTOM); | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | GamePanel::~GamePanel() { | |
| 298 | + | //// | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | void GamePanel::OnPaint(wxPaintEvent&) { | |
| 302 | + | wxBufferedPaintDC pdc(this); | |
| 303 | + | Render(pdc); | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | void GamePanel::OnKeyDown(wxKeyEvent& event) { | |
| 307 | + | switch (event.GetKeyCode()) { | |
| 308 | + | case WXK_ESCAPE: { | |
| 309 | + | if (dead) { | |
| 310 | + | return; | |
| 311 | + | } | |
| 312 | + | wxDialog* dlg = (wxDialog*)GetParent(); | |
| 313 | + | dlg->EndModal(0); | |
| 314 | + | break; | |
| 315 | + | } | |
| 316 | + | default: { | |
| 317 | + | OnKey(event, true); | |
| 318 | + | break; | |
| 319 | + | } | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | void GamePanel::OnKeyUp(wxKeyEvent& event) { | |
| 324 | + | OnKey(event, false); | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | void GamePanel::OnIdle(wxIdleEvent& event) { | |
| 328 | + | int time = game_timer.Time(); | |
| 329 | + | if (time > 1000 / getFPS()) { | |
| 330 | + | game_timer.Start(); | |
| 331 | + | if (!paused()) { | |
| 332 | + | GameLoop(time); | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | if (!paused()) { | |
| 336 | + | event.RequestMore(true); | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | //============================================================================= | |
| 341 | + | // TetrisPanel - A window with a Tetris game! | |
| 342 | + | ||
| 343 | + | TetrisPanel::TetrisPanel(wxWindow* parent) : | |
| 344 | + | GamePanel(parent, 16 * TETRIS_MAPWIDTH, 16 * TETRIS_MAPHEIGHT) { | |
| 345 | + | NewGame(); | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | TetrisPanel::~TetrisPanel() { | |
| 349 | + | //// | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | const wxBrush& TetrisPanel::GetBrush(Color color) const { | |
| 353 | + | static std::unique_ptr<wxBrush> yellow_brush; | |
| 354 | + | static std::unique_ptr<wxBrush> purple_brush; | |
| 355 | + | ||
| 356 | + | if (yellow_brush.get() == nullptr) { | |
| 357 | + | yellow_brush.reset(newd wxBrush(wxColor(255, 255, 0))); | |
| 358 | + | } | |
| 359 | + | if (purple_brush.get() == nullptr) { | |
| 360 | + | purple_brush.reset(newd wxBrush(wxColor(128, 0, 255))); | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | const wxBrush* brush = nullptr; | |
| 364 | + | switch (color) { | |
| 365 | + | case RED: | |
| 366 | + | brush = wxRED_BRUSH; | |
| 367 | + | break; | |
| 368 | + | case BLUE: | |
| 369 | + | brush = wxCYAN_BRUSH; | |
| 370 | + | break; | |
| 371 | + | case GREEN: | |
| 372 | + | brush = wxGREEN_BRUSH; | |
| 373 | + | break; | |
| 374 | + | case PURPLE: | |
| 375 | + | brush = purple_brush.get(); | |
| 376 | + | break; | |
| 377 | + | case YELLOW: | |
| 378 | + | brush = yellow_brush.get(); | |
| 379 | + | break; | |
| 380 | + | case WHITE: | |
| 381 | + | brush = wxWHITE_BRUSH; | |
| 382 | + | break; | |
| 383 | + | case STEEL: | |
| 384 | + | brush = wxGREY_BRUSH; | |
| 385 | + | break; | |
| 386 | + | default: | |
| 387 | + | brush = wxBLACK_BRUSH; | |
| 388 | + | break; | |
| 389 | + | } | |
| 390 | + | return *brush; | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | void TetrisPanel::Render(wxDC& pdc) { | |
| 394 | + | pdc.SetBackground(*wxBLACK_BRUSH); | |
| 395 | + | pdc.Clear(); | |
| 396 | + | ||
| 397 | + | for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) { | |
| 398 | + | for (int x = 0; x < TETRIS_MAPWIDTH; ++x) { | |
| 399 | + | pdc.SetBrush(GetBrush(map[x][y])); | |
| 400 | + | pdc.DrawRectangle(x * 16, y * 16, 16, 16); | |
| 401 | + | } | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | for (int y = 0; y < 4; ++y) { | |
| 405 | + | for (int x = 0; x < 4; ++x) { | |
| 406 | + | if (block.structure[x][y] != NO_COLOR) { | |
| 407 | + | pdc.SetBrush(GetBrush(block.structure[x][y])); | |
| 408 | + | pdc.DrawRectangle((block.x + x) * 16, (block.y + y) * 16, 16, 16); | |
| 409 | + | } | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | void TetrisPanel::OnKey(wxKeyEvent& event, bool down) { | |
| 415 | + | if (!down || dead) { | |
| 416 | + | return; | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | switch (event.GetKeyCode()) { | |
| 420 | + | case WXK_SPACE: { | |
| 421 | + | if (paused()) { | |
| 422 | + | unpause(); | |
| 423 | + | } else { | |
| 424 | + | pause(); | |
| 425 | + | } | |
| 426 | + | break; | |
| 427 | + | } | |
| 428 | + | case WXK_NUMPAD_UP: | |
| 429 | + | case WXK_UP: { | |
| 430 | + | if (dead) { | |
| 431 | + | return; | |
| 432 | + | } | |
| 433 | + | unpause(); | |
| 434 | + | RotateBlock(); | |
| 435 | + | break; | |
| 436 | + | } | |
| 437 | + | case WXK_NUMPAD_DOWN: | |
| 438 | + | case WXK_DOWN: { | |
| 439 | + | if (dead) { | |
| 440 | + | return; | |
| 441 | + | } | |
| 442 | + | unpause(); | |
| 443 | + | MoveBlock(0, 1); | |
| 444 | + | break; | |
| 445 | + | } | |
| 446 | + | case WXK_NUMPAD_LEFT: | |
| 447 | + | case WXK_LEFT: { | |
| 448 | + | if (dead) { | |
| 449 | + | return; | |
| 450 | + | } | |
| 451 | + | unpause(); | |
| 452 | + | MoveBlock(-1, 0); | |
| 453 | + | break; | |
| 454 | + | } | |
| 455 | + | case WXK_NUMPAD_RIGHT: | |
| 456 | + | case WXK_RIGHT: { | |
| 457 | + | if (dead) { | |
| 458 | + | return; | |
| 459 | + | } | |
| 460 | + | unpause(); | |
| 461 | + | MoveBlock(1, 0); | |
| 462 | + | break; | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | void TetrisPanel::NewGame() { | |
| 468 | + | NewBlock(); | |
| 469 | + | score = 0; | |
| 470 | + | lines = 0; | |
| 471 | + | game_timer.Start(); | |
| 472 | + | unpause(); | |
| 473 | + | dead = false; | |
| 474 | + | ||
| 475 | + | // Clear map | |
| 476 | + | for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) { | |
| 477 | + | for (int x = 0; x < TETRIS_MAPWIDTH; ++x) { | |
| 478 | + | map[x][y] = NO_COLOR; | |
| 479 | + | } | |
| 480 | + | } | |
| 481 | + | AddScore(0); // Update title | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | void TetrisPanel::AddScore(int lines_added) { | |
| 485 | + | lines += lines_added; | |
| 486 | + | score += lines_added * lines_added * 10; | |
| 487 | + | wxString title = "Remere's Tetris : "; | |
| 488 | + | title << score << " points "; | |
| 489 | + | title << lines << " lines"; | |
| 490 | + | ((wxTopLevelWindow*)GetParent())->SetTitle(title); | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | void TetrisPanel::GameLoop(int time) { | |
| 494 | + | MoveBlock(0, 1); | |
| 495 | + | } | |
| 496 | + | ||
| 497 | + | bool TetrisPanel::BlockCollisionTest(int mx, int my) const { | |
| 498 | + | int nx = block.x + mx; | |
| 499 | + | int ny = block.y + my; | |
| 500 | + | ||
| 501 | + | for (int y = 0; y < 4; ++y) { | |
| 502 | + | for (int x = 0; x < 4; ++x) { | |
| 503 | + | if (block.structure[x][y] != NO_COLOR) { | |
| 504 | + | if (nx + x < 0 || nx + x > TETRIS_MAPWIDTH - 1 || ny + y < 0 || ny + y > TETRIS_MAPHEIGHT - 1) { | |
| 505 | + | return true; | |
| 506 | + | } | |
| 507 | + | } | |
| 508 | + | } | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) { | |
| 512 | + | for (int x = 0; x < TETRIS_MAPWIDTH; ++x) { | |
| 513 | + | if (x >= nx && x < nx + 4 && y >= ny && y < ny + 4) { | |
| 514 | + | if (map[x][y] != NO_COLOR && block.structure[x - nx][y - ny] != NO_COLOR) { | |
| 515 | + | return true; | |
| 516 | + | } | |
| 517 | + | } | |
| 518 | + | } | |
| 519 | + | } | |
| 520 | + | return false; | |
| 521 | + | } | |
| 522 | + | ||
| 523 | + | void TetrisPanel::RemoveRow(int row) { | |
| 524 | + | for (int x = 0; x < TETRIS_MAPWIDTH; ++x) { | |
| 525 | + | for (int y = row; y > 0; --y) { // Move all above one step down | |
| 526 | + | map[x][y] = map[x][y - 1]; | |
| 527 | + | } | |
| 528 | + | } | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | void TetrisPanel::NewBlock() { | |
| 532 | + | block.x = TETRIS_MAPWIDTH / 2; | |
| 533 | + | block.y = -1; | |
| 534 | + | ||
| 535 | + | for (int y = 0; y < 4; ++y) { | |
| 536 | + | for (int x = 0; x < 4; ++x) { | |
| 537 | + | block.structure[x][y] = NO_COLOR; | |
| 538 | + | } | |
| 539 | + | } | |
| 540 | + | ||
| 541 | + | switch (random(FIRST_BLOCK, LAST_BLOCK)) { | |
| 542 | + | case BLOCK_TOWER: { | |
| 543 | + | block.structure[1][0] = RED; | |
| 544 | + | block.structure[1][1] = RED; | |
| 545 | + | block.structure[1][2] = RED; | |
| 546 | + | block.structure[1][3] = RED; | |
| 547 | + | block.y = 0; | |
| 548 | + | break; | |
| 549 | + | } | |
| 550 | + | default: | |
| 551 | + | case BLOCK_SQUARE: { | |
| 552 | + | block.structure[1][1] = BLUE; | |
| 553 | + | block.structure[1][2] = BLUE; | |
| 554 | + | block.structure[2][1] = BLUE; | |
| 555 | + | block.structure[2][2] = BLUE; | |
| 556 | + | break; | |
| 557 | + | } | |
| 558 | + | case BLOCK_TRIANGLE: { | |
| 559 | + | block.structure[1][1] = STEEL; | |
| 560 | + | block.structure[0][2] = STEEL; | |
| 561 | + | block.structure[1][2] = STEEL; | |
| 562 | + | block.structure[2][2] = STEEL; | |
| 563 | + | break; | |
| 564 | + | } | |
| 565 | + | case BLOCK_Z: { | |
| 566 | + | block.structure[0][1] = YELLOW; | |
| 567 | + | block.structure[1][1] = YELLOW; | |
| 568 | + | block.structure[1][2] = YELLOW; | |
| 569 | + | block.structure[2][2] = YELLOW; | |
| 570 | + | break; | |
| 571 | + | } | |
| 572 | + | case BLOCK_S: { | |
| 573 | + | block.structure[2][1] = GREEN; | |
| 574 | + | block.structure[1][1] = GREEN; | |
| 575 | + | block.structure[1][2] = GREEN; | |
| 576 | + | block.structure[0][2] = GREEN; | |
| 577 | + | break; | |
| 578 | + | } | |
| 579 | + | case BLOCK_J: { | |
| 580 | + | block.structure[1][1] = WHITE; | |
| 581 | + | block.structure[2][1] = WHITE; | |
| 582 | + | block.structure[2][2] = WHITE; | |
| 583 | + | block.structure[2][3] = WHITE; | |
| 584 | + | break; | |
| 585 | + | } | |
| 586 | + | case BLOCK_L: { | |
| 587 | + | block.structure[2][1] = PURPLE; | |
| 588 | + | block.structure[1][1] = PURPLE; | |
| 589 | + | block.structure[1][2] = PURPLE; | |
| 590 | + | block.structure[1][3] = PURPLE; | |
| 591 | + | // break; missing add it? | |
| 592 | + | } | |
| 593 | + | } | |
| 594 | + | } | |
| 595 | + | ||
| 596 | + | void TetrisPanel::MoveBlock(int x, int y) { | |
| 597 | + | if (BlockCollisionTest(x, y)) { | |
| 598 | + | if (y == 1) { // moving down... | |
| 599 | + | if (block.y < 1) { // Out of bounds! | |
| 600 | + | dead = true; | |
| 601 | + | g_gui.PopupDialog("Game Over", "You reached a score of " + i2ws(score) + "!", wxOK); | |
| 602 | + | NewGame(); | |
| 603 | + | SetFocus(); | |
| 604 | + | } else { | |
| 605 | + | // Freeze the old block onto the map | |
| 606 | + | for (int y = 0; y < 4; ++y) { | |
| 607 | + | for (int x = 0; x < 4; ++x) { | |
| 608 | + | if (block.structure[x][y] != NO_COLOR) { | |
| 609 | + | map[block.x + x][block.y + y] = block.structure[x][y]; | |
| 610 | + | } | |
| 611 | + | } | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | // Any cleared rows? | |
| 615 | + | int cleared = 0; | |
| 616 | + | for (int y = 0; y < TETRIS_MAPHEIGHT; ++y) { | |
| 617 | + | bool full = true; | |
| 618 | + | for (int x = 0; x < TETRIS_MAPWIDTH; ++x) { | |
| 619 | + | if (map[x][y] == NO_COLOR) { | |
| 620 | + | full = false; | |
| 621 | + | } | |
| 622 | + | } | |
| 623 | + | if (full) { | |
| 624 | + | RemoveRow(y); | |
| 625 | + | ++cleared; | |
| 626 | + | } | |
| 627 | + | } | |
| 628 | + | AddScore(cleared); | |
| 629 | + | NewBlock(); | |
| 630 | + | } | |
| 631 | + | } // If we're not moving down, we're not moving the block either | |
| 632 | + | } else { | |
| 633 | + | // No collision so move the block! | |
| 634 | + | block.x += x; | |
| 635 | + | block.y += y; | |
| 636 | + | } | |
| 637 | + | ||
| 638 | + | Refresh(); | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | void TetrisPanel::RotateBlock() { | |
| 642 | + | Block temp; | |
| 643 | + | ||
| 644 | + | for (int y = 0; y < 4; ++y) { | |
| 645 | + | for (int x = 0; x < 4; ++x) { | |
| 646 | + | temp.structure[3 - x][y] = block.structure[y][x]; | |
| 647 | + | } | |
| 648 | + | } | |
| 649 | + | ||
| 650 | + | for (int y = 0; y < 4; ++y) { | |
| 651 | + | for (int x = 0; x < 4; ++x) { | |
| 652 | + | if (temp.structure[x][y] != NO_COLOR) { | |
| 653 | + | if (block.x + x < 0 || block.x + x > TETRIS_MAPWIDTH - 1 || block.y + y < 0 || block.y + y > TETRIS_MAPHEIGHT - 1) { | |
| 654 | + | return; | |
| 655 | + | } | |
| 656 | + | } | |
| 657 | + | } | |
| 658 | + | } | |
| 659 | + | ||
| 660 | + | for (int y = 0; y < TETRIS_MAPWIDTH; ++y) { | |
| 661 | + | for (int x = 0; x < TETRIS_MAPHEIGHT; ++x) { | |
| 662 | + | if (x >= block.x && x < block.x + 4 && y >= block.y && y < block.y + 4) { | |
| 663 | + | if (map[x][y] != NO_COLOR && temp.structure[x - block.x][y - block.y] != NO_COLOR) { | |
| 664 | + | return; | |
| 665 | + | } | |
| 666 | + | } | |
| 667 | + | } | |
| 668 | + | } | |
| 669 | + | ||
| 670 | + | for (int y = 0; y < 4; ++y) { | |
| 671 | + | for (int x = 0; x < 4; ++x) { | |
| 672 | + | block.structure[x][y] = temp.structure[x][y]; | |
| 673 | + | } | |
| 674 | + | } | |
| 675 | + | ||
| 676 | + | Refresh(); | |
| 677 | + | } | |
| 678 | + | ||
| 679 | + | //============================================================================= | |
| 680 | + | // SnakePanel - A window with a Snake game! | |
| 681 | + | ||
| 682 | + | SnakePanel::SnakePanel(wxWindow* parent) : | |
| 683 | + | GamePanel(parent, 16 * SNAKE_MAPWIDTH, 16 * SNAKE_MAPHEIGHT) { | |
| 684 | + | NewGame(); | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | SnakePanel::~SnakePanel() { | |
| 688 | + | //// | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | void SnakePanel::Render(wxDC& pdc) { | |
| 692 | + | pdc.SetBackground(*wxBLACK_BRUSH); | |
| 693 | + | pdc.Clear(); | |
| 694 | + | ||
| 695 | + | wxBrush snakebrush(wxColor(0, 0, 255)); | |
| 696 | + | wxBrush applebrush(wxColor(255, 0, 0)); | |
| 697 | + | ||
| 698 | + | double lblue = 1.0; | |
| 699 | + | double lred = 0.5; | |
| 700 | + | double lgreen = 0.0; | |
| 701 | + | ||
| 702 | + | for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) { | |
| 703 | + | for (int x = 0; x < SNAKE_MAPWIDTH; ++x) { | |
| 704 | + | if (map[x][y] == -1) { // Apple | |
| 705 | + | pdc.SetBrush(applebrush); | |
| 706 | + | pdc.DrawRectangle(x * 16, y * 16, 16, 16); | |
| 707 | + | } else if (map[x][y] > 0) { // Snake | |
| 708 | + | double snook = double(map[x][y]) / length; | |
| 709 | + | snakebrush.SetColour(wxColor( | |
| 710 | + | int(255.0 * (1.0 - abs(lred - snook))), | |
| 711 | + | int(255.0 * (1.0 - abs(lgreen - snook))), | |
| 712 | + | int(255.0 * (1.0 - abs(lblue - snook))) | |
| 713 | + | ) | |
| 714 | + | ); | |
| 715 | + | pdc.SetBrush(snakebrush); | |
| 716 | + | pdc.DrawRectangle(x * 16, y * 16, 16, 16); | |
| 717 | + | } | |
| 718 | + | } | |
| 719 | + | } | |
| 720 | + | } | |
| 721 | + | ||
| 722 | + | void SnakePanel::OnKey(wxKeyEvent& event, bool down) { | |
| 723 | + | if (!down) { | |
| 724 | + | return; | |
| 725 | + | } | |
| 726 | + | ||
| 727 | + | int keyCode = event.GetKeyCode(); | |
| 728 | + | if (keyCode == WXK_SPACE) { | |
| 729 | + | if (paused()) { | |
| 730 | + | unpause(); | |
| 731 | + | } else { | |
| 732 | + | pause(); | |
| 733 | + | } | |
| 734 | + | } else if (!dead) { | |
| 735 | + | switch (keyCode) { | |
| 736 | + | case WXK_NUMPAD_UP: | |
| 737 | + | case WXK_UP: { | |
| 738 | + | unpause(); | |
| 739 | + | Move(NORTH); | |
| 740 | + | break; | |
| 741 | + | } | |
| 742 | + | case WXK_NUMPAD_DOWN: | |
| 743 | + | case WXK_DOWN: { | |
| 744 | + | unpause(); | |
| 745 | + | Move(SOUTH); | |
| 746 | + | break; | |
| 747 | + | } | |
| 748 | + | case WXK_NUMPAD_LEFT: | |
| 749 | + | case WXK_LEFT: { | |
| 750 | + | unpause(); | |
| 751 | + | Move(WEST); | |
| 752 | + | break; | |
| 753 | + | } | |
| 754 | + | case WXK_NUMPAD_RIGHT: | |
| 755 | + | case WXK_RIGHT: { | |
| 756 | + | unpause(); | |
| 757 | + | Move(EAST); | |
| 758 | + | break; | |
| 759 | + | } | |
| 760 | + | } | |
| 761 | + | } | |
| 762 | + | } | |
| 763 | + | ||
| 764 | + | void SnakePanel::NewGame() { | |
| 765 | + | length = 3; | |
| 766 | + | game_timer.Start(); | |
| 767 | + | last_dir = NORTH; | |
| 768 | + | unpause(); | |
| 769 | + | dead = false; | |
| 770 | + | ||
| 771 | + | // Clear map | |
| 772 | + | for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) { | |
| 773 | + | for (int x = 0; x < SNAKE_MAPWIDTH; ++x) { | |
| 774 | + | map[x][y] = 0; | |
| 775 | + | } | |
| 776 | + | } | |
| 777 | + | map[SNAKE_MAPWIDTH / 2][SNAKE_MAPHEIGHT / 2] = length; | |
| 778 | + | NewApple(); | |
| 779 | + | UpdateTitle(); // Update title | |
| 780 | + | } | |
| 781 | + | ||
| 782 | + | void SnakePanel::UpdateTitle() { | |
| 783 | + | wxString title = "Remere's Snake : "; | |
| 784 | + | title << length << " segments"; | |
| 785 | + | ((wxTopLevelWindow*)GetParent())->SetTitle(title); | |
| 786 | + | } | |
| 787 | + | ||
| 788 | + | void SnakePanel::GameLoop(int time) { | |
| 789 | + | Move(last_dir); | |
| 790 | + | } | |
| 791 | + | ||
| 792 | + | void SnakePanel::NewApple() { | |
| 793 | + | bool possible = false; | |
| 794 | + | for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) { | |
| 795 | + | for (int x = 0; x < SNAKE_MAPWIDTH; ++x) { | |
| 796 | + | if (map[x][y] == 0) { | |
| 797 | + | possible = true; | |
| 798 | + | } | |
| 799 | + | if (possible) { | |
| 800 | + | break; | |
| 801 | + | } | |
| 802 | + | } | |
| 803 | + | if (possible) { | |
| 804 | + | break; | |
| 805 | + | } | |
| 806 | + | } | |
| 807 | + | ||
| 808 | + | if (possible) { | |
| 809 | + | while (true) { | |
| 810 | + | int x = random(0, SNAKE_MAPWIDTH - 1); | |
| 811 | + | int y = random(0, SNAKE_MAPHEIGHT - 1); | |
| 812 | + | if (map[x][y] == 0) { | |
| 813 | + | map[x][y] = -1; | |
| 814 | + | break; | |
| 815 | + | } | |
| 816 | + | } | |
| 817 | + | } | |
| 818 | + | } | |
| 819 | + | ||
| 820 | + | void SnakePanel::Move(int dir) { | |
| 821 | + | if ((last_dir == NORTH && dir == SOUTH) || (last_dir == WEST && dir == EAST) || (last_dir == EAST && dir == WEST) || (last_dir == SOUTH && dir == NORTH)) { | |
| 822 | + | return; | |
| 823 | + | } | |
| 824 | + | ||
| 825 | + | int nx = 0, ny = 0; | |
| 826 | + | int head_x = 0, head_y = 0; | |
| 827 | + | for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) { | |
| 828 | + | for (int x = 0; x < SNAKE_MAPWIDTH; ++x) { | |
| 829 | + | if (map[x][y] == length) { | |
| 830 | + | head_x = x; | |
| 831 | + | head_y = y; | |
| 832 | + | } | |
| 833 | + | } | |
| 834 | + | } | |
| 835 | + | switch (dir) { | |
| 836 | + | case NORTH: { | |
| 837 | + | nx = head_x; | |
| 838 | + | ny = head_y - 1; | |
| 839 | + | break; | |
| 840 | + | } | |
| 841 | + | case SOUTH: { | |
| 842 | + | nx = head_x; | |
| 843 | + | ny = head_y + 1; | |
| 844 | + | break; | |
| 845 | + | } | |
| 846 | + | case WEST: { | |
| 847 | + | nx = head_x - 1; | |
| 848 | + | ny = head_y; | |
| 849 | + | break; | |
| 850 | + | } | |
| 851 | + | case EAST: { | |
| 852 | + | nx = head_x + 1; | |
| 853 | + | ny = head_y; | |
| 854 | + | break; | |
| 855 | + | } | |
| 856 | + | default: | |
| 857 | + | return; | |
| 858 | + | } | |
| 859 | + | ||
| 860 | + | if (map[nx][ny] > 0 || nx < 0 || ny < 0 || nx >= SNAKE_MAPWIDTH || ny >= SNAKE_MAPHEIGHT) { | |
| 861 | + | // Crash | |
| 862 | + | dead = true; | |
| 863 | + | g_gui.PopupDialog("Game Over", "You reached a length of " + i2ws(length) + "!", wxOK); | |
| 864 | + | NewGame(); | |
| 865 | + | SetFocus(); | |
| 866 | + | } else { | |
| 867 | + | // Walk! | |
| 868 | + | if (map[nx][ny] == -1) { | |
| 869 | + | // Took apple! | |
| 870 | + | length += 1; | |
| 871 | + | UpdateTitle(); | |
| 872 | + | NewApple(); | |
| 873 | + | } else { | |
| 874 | + | for (int y = 0; y < SNAKE_MAPHEIGHT; ++y) { | |
| 875 | + | for (int x = 0; x < SNAKE_MAPWIDTH; ++x) { | |
| 876 | + | if (map[x][y] > 0) { | |
| 877 | + | map[x][y] -= 1; | |
| 878 | + | } | |
| 879 | + | } | |
| 880 | + | } | |
| 881 | + | } | |
| 882 | + | map[nx][ny] = length; | |
| 883 | + | } | |
| 884 | + | last_dir = dir; | |
| 885 | + | ||
| 886 | + | Refresh(); | |
| 887 | + | } |
| @@ -0,0 +1,43 @@ | |||
| 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_ABOUT_WINDOW_H_ | |
| 19 | + | #define RME_ABOUT_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "main.h" | |
| 22 | + | ||
| 23 | + | class GamePanel; | |
| 24 | + | ||
| 25 | + | class AboutWindow : public wxDialog { | |
| 26 | + | public: | |
| 27 | + | AboutWindow(wxWindow* parent); | |
| 28 | + | ~AboutWindow(); | |
| 29 | + | ||
| 30 | + | void OnClickOK(wxCommandEvent&); | |
| 31 | + | void OnClickLicense(wxCommandEvent&); | |
| 32 | + | ||
| 33 | + | void OnTetris(wxCommandEvent&); | |
| 34 | + | void OnSnake(wxCommandEvent&); | |
| 35 | + | ||
| 36 | + | private: | |
| 37 | + | wxSizer* topsizer; | |
| 38 | + | GamePanel* game_panel; | |
| 39 | + | ||
| 40 | + | DECLARE_EVENT_TABLE() | |
| 41 | + | }; | |
| 42 | + | ||
| 43 | + | #endif |
| @@ -0,0 +1,754 @@ | |||
| 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 "action.h" | |
| 21 | + | #include "settings.h" | |
| 22 | + | #include "map.h" | |
| 23 | + | #include "editor.h" | |
| 24 | + | #include "gui.h" | |
| 25 | + | ||
| 26 | + | // Add necessary includes for exception handling and file operations | |
| 27 | + | #include <exception> | |
| 28 | + | #include <fstream> | |
| 29 | + | #include <wx/filename.h> | |
| 30 | + | #include <wx/datetime.h> | |
| 31 | + | #include <wx/stdpaths.h> | |
| 32 | + | ||
| 33 | + | Change::Change() : | |
| 34 | + | type(CHANGE_NONE), data(nullptr) { | |
| 35 | + | //// | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | Change::Change(Tile* t) : | |
| 39 | + | type(CHANGE_TILE) { | |
| 40 | + | ASSERT(t); | |
| 41 | + | data = t; | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | Change* Change::Create(House* house, const Position& where) { | |
| 45 | + | Change* c = newd Change(); | |
| 46 | + | c->type = CHANGE_MOVE_HOUSE_EXIT; | |
| 47 | + | std::pair<uint32_t, Position>* p = newd std::pair<uint32_t, Position>; | |
| 48 | + | p->first = house->getID(); | |
| 49 | + | p->second = where; | |
| 50 | + | c->data = p; | |
| 51 | + | return c; | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | Change* Change::Create(Waypoint* wp, const Position& where) { | |
| 55 | + | Change* c = newd Change(); | |
| 56 | + | c->type = CHANGE_MOVE_WAYPOINT; | |
| 57 | + | std::pair<std::string, Position>* p = newd std::pair<std::string, Position>; | |
| 58 | + | p->first = wp->name; | |
| 59 | + | p->second = where; | |
| 60 | + | c->data = p; | |
| 61 | + | return c; | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | Change::~Change() { | |
| 65 | + | clear(); | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | void Change::clear() { | |
| 69 | + | switch (type) { | |
| 70 | + | case CHANGE_TILE: | |
| 71 | + | ASSERT(data); | |
| 72 | + | delete reinterpret_cast<Tile*>(data); | |
| 73 | + | break; | |
| 74 | + | case CHANGE_MOVE_HOUSE_EXIT: | |
| 75 | + | ASSERT(data); | |
| 76 | + | delete reinterpret_cast<std::pair<uint32_t, Position>*>(data); | |
| 77 | + | break; | |
| 78 | + | case CHANGE_MOVE_WAYPOINT: | |
| 79 | + | ASSERT(data); | |
| 80 | + | delete reinterpret_cast<std::pair<std::string, Position>*>(data); | |
| 81 | + | break; | |
| 82 | + | case CHANGE_NONE: | |
| 83 | + | break; | |
| 84 | + | default: | |
| 85 | + | #ifdef __DEBUG_MODE__ | |
| 86 | + | if (data) { | |
| 87 | + | printf("UNHANDLED CHANGE TYPE! Leak!"); | |
| 88 | + | } | |
| 89 | + | #endif | |
| 90 | + | break; | |
| 91 | + | } | |
| 92 | + | type = CHANGE_NONE; | |
| 93 | + | data = nullptr; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | uint32_t Change::memsize() const { | |
| 97 | + | uint32_t mem = sizeof(*this); | |
| 98 | + | switch (type) { | |
| 99 | + | case CHANGE_TILE: | |
| 100 | + | ASSERT(data); | |
| 101 | + | mem += reinterpret_cast<Tile*>(data)->memsize(); | |
| 102 | + | break; | |
| 103 | + | default: | |
| 104 | + | break; | |
| 105 | + | } | |
| 106 | + | return mem; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | Action::Action(Editor& editor, ActionIdentifier ident) : | |
| 110 | + | commited(false), | |
| 111 | + | editor(editor), | |
| 112 | + | type(ident) { | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | Action::~Action() { | |
| 116 | + | ChangeList::const_reverse_iterator it = changes.rbegin(); | |
| 117 | + | while (it != changes.rend()) { | |
| 118 | + | delete *it; | |
| 119 | + | ++it; | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | size_t Action::approx_memsize() const { | |
| 124 | + | uint32_t mem = sizeof(*this); | |
| 125 | + | mem += changes.size() * (sizeof(Change) + sizeof(Tile) + sizeof(Item) + 6 /* approx overhead*/); | |
| 126 | + | return mem; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | size_t Action::memsize() const { | |
| 130 | + | uint32_t mem = sizeof(*this); | |
| 131 | + | mem += sizeof(Change*) * 3 * changes.size(); | |
| 132 | + | ChangeList::const_iterator it = changes.begin(); | |
| 133 | + | while (it != changes.end()) { | |
| 134 | + | Change* c = *it; | |
| 135 | + | switch (c->type) { | |
| 136 | + | case CHANGE_TILE: { | |
| 137 | + | ASSERT(c->data); | |
| 138 | + | mem += reinterpret_cast<Tile*>(c->data)->memsize(); | |
| 139 | + | break; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | default: | |
| 143 | + | break; | |
| 144 | + | } | |
| 145 | + | ++it; | |
| 146 | + | } | |
| 147 | + | return mem; | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | void Action::commit(DirtyList* dirty_list) { | |
| 151 | + | editor.selection.start(Selection::INTERNAL); | |
| 152 | + | ChangeList::const_iterator it = changes.begin(); | |
| 153 | + | while (it != changes.end()) { | |
| 154 | + | Change* c = *it; | |
| 155 | + | switch (c->type) { | |
| 156 | + | case CHANGE_TILE: { | |
| 157 | + | void** data = &c->data; | |
| 158 | + | Tile* newtile = reinterpret_cast<Tile*>(*data); | |
| 159 | + | ASSERT(newtile); | |
| 160 | + | Position pos = newtile->getPosition(); | |
| 161 | + | ||
| 162 | + | if (editor.IsLiveClient()) { | |
| 163 | + | QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y); | |
| 164 | + | if (!nd || !nd->isVisible(pos.z > GROUND_LAYER)) { | |
| 165 | + | // Delete all changes that affect tiles outside our view | |
| 166 | + | c->clear(); | |
| 167 | + | ++it; | |
| 168 | + | continue; | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | Tile* oldtile = editor.map.swapTile(pos, newtile); | |
| 173 | + | TileLocation* location = newtile->getLocation(); | |
| 174 | + | ||
| 175 | + | // Update other nodes in the network | |
| 176 | + | if (editor.IsLiveServer() && dirty_list) { | |
| 177 | + | dirty_list->AddPosition(pos.x, pos.y, pos.z); | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | newtile->update(); | |
| 181 | + | ||
| 182 | + | // std::cout << "\tSwitched tile at " << pos.x << ";" << pos.y << ";" << pos.z << " from " << (void*)oldtile << " to " << *data << std::endl; | |
| 183 | + | if (newtile->isSelected()) { | |
| 184 | + | editor.selection.addInternal(newtile); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | if (oldtile) { | |
| 188 | + | if (newtile->getHouseID() != oldtile->getHouseID()) { | |
| 189 | + | // oooooomggzzz we need to add it to the appropriate house! | |
| 190 | + | House* house = editor.map.houses.getHouse(oldtile->getHouseID()); | |
| 191 | + | if (house) { | |
| 192 | + | house->removeTile(oldtile); | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | house = editor.map.houses.getHouse(newtile->getHouseID()); | |
| 196 | + | if (house) { | |
| 197 | + | house->addTile(newtile); | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | if (oldtile->spawn) { | |
| 201 | + | if (newtile->spawn) { | |
| 202 | + | if (*oldtile->spawn != *newtile->spawn) { | |
| 203 | + | editor.map.removeSpawn(oldtile); | |
| 204 | + | editor.map.addSpawn(newtile); | |
| 205 | + | } | |
| 206 | + | } else { | |
| 207 | + | // Spawn has been removed | |
| 208 | + | editor.map.removeSpawn(oldtile); | |
| 209 | + | } | |
| 210 | + | } else if (newtile->spawn) { | |
| 211 | + | editor.map.addSpawn(newtile); | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // oldtile->update(); | |
| 215 | + | if (oldtile->isSelected()) { | |
| 216 | + | editor.selection.removeInternal(oldtile); | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | *data = oldtile; | |
| 220 | + | } else { | |
| 221 | + | *data = editor.map.allocator(location); | |
| 222 | + | if (newtile->getHouseID() != 0) { | |
| 223 | + | // oooooomggzzz we need to add it to the appropriate house! | |
| 224 | + | House* house = editor.map.houses.getHouse(newtile->getHouseID()); | |
| 225 | + | if (house) { | |
| 226 | + | house->addTile(newtile); | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | if (newtile->spawn) { | |
| 231 | + | editor.map.addSpawn(newtile); | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | // Mark the tile as modified | |
| 235 | + | newtile->modify(); | |
| 236 | + | ||
| 237 | + | // Update client dirty list | |
| 238 | + | if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { | |
| 239 | + | // Local action, assemble changes | |
| 240 | + | dirty_list->AddChange(c); | |
| 241 | + | } | |
| 242 | + | break; | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | case CHANGE_MOVE_HOUSE_EXIT: { | |
| 246 | + | std::pair<uint32_t, Position>* p = reinterpret_cast<std::pair<uint32_t, Position>*>(c->data); | |
| 247 | + | ASSERT(p); | |
| 248 | + | House* whathouse = editor.map.houses.getHouse(p->first); | |
| 249 | + | ||
| 250 | + | if (whathouse) { | |
| 251 | + | Position oldpos = whathouse->getExit(); | |
| 252 | + | whathouse->setExit(p->second); | |
| 253 | + | p->second = oldpos; | |
| 254 | + | } | |
| 255 | + | break; | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | case CHANGE_MOVE_WAYPOINT: { | |
| 259 | + | std::pair<std::string, Position>* p = reinterpret_cast<std::pair<std::string, Position>*>(c->data); | |
| 260 | + | ASSERT(p); | |
| 261 | + | Waypoint* wp = editor.map.waypoints.getWaypoint(p->first); | |
| 262 | + | ||
| 263 | + | if (wp) { | |
| 264 | + | // Change the tiles | |
| 265 | + | TileLocation* oldtile = editor.map.getTileL(wp->pos); | |
| 266 | + | TileLocation* newtile = editor.map.getTileL(p->second); | |
| 267 | + | ||
| 268 | + | // Only need to remove from old if it actually exists | |
| 269 | + | if (p->second != Position()) { | |
| 270 | + | if (oldtile && oldtile->getWaypointCount() > 0) { | |
| 271 | + | oldtile->decreaseWaypointCount(); | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | newtile->increaseWaypointCount(); | |
| 276 | + | ||
| 277 | + | // Update shit | |
| 278 | + | Position oldpos = wp->pos; | |
| 279 | + | wp->pos = p->second; | |
| 280 | + | p->second = oldpos; | |
| 281 | + | } | |
| 282 | + | break; | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | default: | |
| 286 | + | break; | |
| 287 | + | } | |
| 288 | + | ++it; | |
| 289 | + | } | |
| 290 | + | editor.selection.finish(Selection::INTERNAL); | |
| 291 | + | commited = true; | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | void Action::undo(DirtyList* dirty_list) { | |
| 295 | + | if (changes.empty()) { | |
| 296 | + | return; | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | editor.selection.start(Selection::INTERNAL); | |
| 300 | + | ChangeList::reverse_iterator it = changes.rbegin(); | |
| 301 | + | ||
| 302 | + | while (it != changes.rend()) { | |
| 303 | + | Change* c = *it; | |
| 304 | + | switch (c->type) { | |
| 305 | + | case CHANGE_TILE: { | |
| 306 | + | void** data = &c->data; | |
| 307 | + | Tile* oldtile = reinterpret_cast<Tile*>(*data); | |
| 308 | + | ASSERT(oldtile); | |
| 309 | + | Position pos = oldtile->getPosition(); | |
| 310 | + | ||
| 311 | + | if (editor.IsLiveClient()) { | |
| 312 | + | QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y); | |
| 313 | + | if (!nd || !nd->isVisible(pos.z > GROUND_LAYER)) { | |
| 314 | + | // Delete all changes that affect tiles outside our view | |
| 315 | + | c->clear(); | |
| 316 | + | ++it; | |
| 317 | + | continue; | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | Tile* newtile = editor.map.swapTile(pos, oldtile); | |
| 322 | + | ||
| 323 | + | // Update server side change list (for broadcast) | |
| 324 | + | if (editor.IsLiveServer() && dirty_list) { | |
| 325 | + | dirty_list->AddPosition(pos.x, pos.y, pos.z); | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | if (oldtile->isSelected()) { | |
| 329 | + | editor.selection.addInternal(oldtile); | |
| 330 | + | } | |
| 331 | + | if (newtile->isSelected()) { | |
| 332 | + | editor.selection.removeInternal(newtile); | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | if (newtile->getHouseID() != oldtile->getHouseID()) { | |
| 336 | + | // oooooomggzzz we need to remove it from the appropriate house! | |
| 337 | + | House* house = editor.map.houses.getHouse(newtile->getHouseID()); | |
| 338 | + | if (house) { | |
| 339 | + | house->removeTile(newtile); | |
| 340 | + | } else { | |
| 341 | + | // Set tile house to 0, house has been removed | |
| 342 | + | newtile->setHouse(nullptr); | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | house = editor.map.houses.getHouse(oldtile->getHouseID()); | |
| 346 | + | if (house) { | |
| 347 | + | house->addTile(oldtile); | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | if (oldtile->spawn) { | |
| 352 | + | if (newtile->spawn) { | |
| 353 | + | if (*oldtile->spawn != *newtile->spawn) { | |
| 354 | + | editor.map.removeSpawn(newtile); | |
| 355 | + | editor.map.addSpawn(oldtile); | |
| 356 | + | } | |
| 357 | + | } else { | |
| 358 | + | editor.map.addSpawn(oldtile); | |
| 359 | + | } | |
| 360 | + | } else if (newtile->spawn) { | |
| 361 | + | editor.map.removeSpawn(newtile); | |
| 362 | + | } | |
| 363 | + | *data = newtile; | |
| 364 | + | ||
| 365 | + | // Update client dirty list | |
| 366 | + | if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { | |
| 367 | + | // Local action, assemble changes | |
| 368 | + | dirty_list->AddChange(c); | |
| 369 | + | } | |
| 370 | + | break; | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | case CHANGE_MOVE_HOUSE_EXIT: { | |
| 374 | + | std::pair<uint32_t, Position>* p = reinterpret_cast<std::pair<uint32_t, Position>*>(c->data); | |
| 375 | + | ASSERT(p); | |
| 376 | + | House* whathouse = editor.map.houses.getHouse(p->first); | |
| 377 | + | if (whathouse) { | |
| 378 | + | Position oldpos = whathouse->getExit(); | |
| 379 | + | whathouse->setExit(p->second); | |
| 380 | + | p->second = oldpos; | |
| 381 | + | } | |
| 382 | + | break; | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | case CHANGE_MOVE_WAYPOINT: { | |
| 386 | + | std::pair<std::string, Position>* p = reinterpret_cast<std::pair<std::string, Position>*>(c->data); | |
| 387 | + | ASSERT(p); | |
| 388 | + | Waypoint* wp = editor.map.waypoints.getWaypoint(p->first); | |
| 389 | + | ||
| 390 | + | if (wp) { | |
| 391 | + | // Change the tiles | |
| 392 | + | TileLocation* oldtile = editor.map.getTileL(wp->pos); | |
| 393 | + | TileLocation* newtile = editor.map.getTileL(p->second); | |
| 394 | + | ||
| 395 | + | // Only need to remove from old if it actually exists | |
| 396 | + | if (p->second != Position()) { | |
| 397 | + | if (oldtile && oldtile->getWaypointCount() > 0) { | |
| 398 | + | oldtile->decreaseWaypointCount(); | |
| 399 | + | } | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | if (newtile) { | |
| 403 | + | newtile->increaseWaypointCount(); | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | // Update shit | |
| 407 | + | Position oldpos = wp->pos; | |
| 408 | + | wp->pos = p->second; | |
| 409 | + | p->second = oldpos; | |
| 410 | + | } | |
| 411 | + | break; | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | default: | |
| 415 | + | break; | |
| 416 | + | } | |
| 417 | + | ++it; | |
| 418 | + | } | |
| 419 | + | editor.selection.finish(Selection::INTERNAL); | |
| 420 | + | commited = false; | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | BatchAction::BatchAction(Editor& editor, ActionIdentifier ident) : | |
| 424 | + | editor(editor), | |
| 425 | + | timestamp(0), | |
| 426 | + | memory_size(0), | |
| 427 | + | type(ident) { | |
| 428 | + | //// | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | BatchAction::~BatchAction() { | |
| 432 | + | for (Action* action : batch) { | |
| 433 | + | delete action; | |
| 434 | + | } | |
| 435 | + | batch.clear(); | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | size_t BatchAction::memsize(bool recalc) const { | |
| 439 | + | // Expensive operation, only evaluate once (won't change anyways) | |
| 440 | + | if (!recalc && memory_size > 0) { | |
| 441 | + | return memory_size; | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | uint32_t mem = sizeof(*this); | |
| 445 | + | mem += sizeof(Action*) * 3 * batch.size(); | |
| 446 | + | ||
| 447 | + | for (Action* action : batch) { | |
| 448 | + | #ifdef __USE_EXACT_MEMSIZE__ | |
| 449 | + | mem += action->memsize(); | |
| 450 | + | #else | |
| 451 | + | // Less exact but MUCH faster | |
| 452 | + | mem += action->approx_memsize(); | |
| 453 | + | #endif | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | const_cast<BatchAction*>(this)->memory_size = mem; | |
| 457 | + | return mem; | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | void BatchAction::addAction(Action* action) { | |
| 461 | + | // If empty, do nothing. | |
| 462 | + | if (action->size() == 0) { | |
| 463 | + | delete action; | |
| 464 | + | return; | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | ASSERT(action->getType() == type); | |
| 468 | + | ||
| 469 | + | if (!editor.CanEdit()) { | |
| 470 | + | delete action; | |
| 471 | + | return; | |
| 472 | + | } | |
| 473 | + | ||
| 474 | + | // Add it! | |
| 475 | + | batch.push_back(action); | |
| 476 | + | timestamp = time(nullptr); | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | void BatchAction::addAndCommitAction(Action* action) { | |
| 480 | + | // If empty, do nothing. | |
| 481 | + | if (action->size() == 0) { | |
| 482 | + | delete action; | |
| 483 | + | return; | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | if (!editor.CanEdit()) { | |
| 487 | + | delete action; | |
| 488 | + | return; | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // Add it! | |
| 492 | + | action->commit(nullptr); | |
| 493 | + | batch.push_back(action); | |
| 494 | + | timestamp = time(nullptr); | |
| 495 | + | } | |
| 496 | + | ||
| 497 | + | void BatchAction::commit() { | |
| 498 | + | for (Action* action : batch) { | |
| 499 | + | if (!action->isCommited()) { | |
| 500 | + | action->commit(nullptr); | |
| 501 | + | } | |
| 502 | + | } | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | void BatchAction::undo() { | |
| 506 | + | for (Action* action : boost::adaptors::reverse(batch)) { | |
| 507 | + | action->undo(nullptr); | |
| 508 | + | } | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | void BatchAction::redo() { | |
| 512 | + | for (Action* action : batch) { | |
| 513 | + | action->redo(nullptr); | |
| 514 | + | } | |
| 515 | + | } | |
| 516 | + | ||
| 517 | + | void BatchAction::merge(BatchAction* other) { | |
| 518 | + | batch.insert(batch.end(), other->batch.begin(), other->batch.end()); | |
| 519 | + | other->batch.clear(); | |
| 520 | + | } | |
| 521 | + | ||
| 522 | + | ActionQueue::ActionQueue(Editor& editor) : | |
| 523 | + | current(0), memory_size(0), editor(editor) { | |
| 524 | + | //// | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | ActionQueue::~ActionQueue() { | |
| 528 | + | for (auto it = actions.begin(); it != actions.end(); /* no increment here */) { | |
| 529 | + | delete *it; | |
| 530 | + | it = actions.erase(it); // erase returns the next valid iterator | |
| 531 | + | } | |
| 532 | + | } | |
| 533 | + | ||
| 534 | + | Action* ActionQueue::createAction(ActionIdentifier ident) { | |
| 535 | + | return newd Action(editor, ident); | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | Action* ActionQueue::createAction(BatchAction* batch) { | |
| 539 | + | return newd Action(editor, batch->getType()); | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | BatchAction* ActionQueue::createBatch(ActionIdentifier ident) { | |
| 543 | + | return newd BatchAction(editor, ident); | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | void ActionQueue::resetTimer() { | |
| 547 | + | if (!actions.empty()) { | |
| 548 | + | actions.back()->resetTimer(); | |
| 549 | + | } | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | void ActionQueue::addAction(Action* action, int stacking_delay) { | |
| 553 | + | // Ensure we're on the main thread | |
| 554 | + | if (!wxThread::IsMain()) { | |
| 555 | + | wxTheApp->CallAfter([=]() { | |
| 556 | + | this->addAction(action, stacking_delay); | |
| 557 | + | }); | |
| 558 | + | return; | |
| 559 | + | } | |
| 560 | + | ||
| 561 | + | BatchAction* batch = createBatch(action->getType()); | |
| 562 | + | batch->addAndCommitAction(action); | |
| 563 | + | if (batch->size() == 0) { | |
| 564 | + | delete batch; | |
| 565 | + | return; | |
| 566 | + | } | |
| 567 | + | ||
| 568 | + | addBatch(batch, stacking_delay); | |
| 569 | + | } | |
| 570 | + | ||
| 571 | + | void ActionQueue::addBatch(BatchAction* batch, int stacking_delay) { | |
| 572 | + | // Ensure we're on the main thread | |
| 573 | + | if (!wxThread::IsMain()) { | |
| 574 | + | // Use CallAfter but with a 'copy' of needed parameters | |
| 575 | + | // to prevent race conditions | |
| 576 | + | ActionQueue* self = this; | |
| 577 | + | int delay = stacking_delay; | |
| 578 | + | wxTheApp->CallAfter([self, batch, delay]() { | |
| 579 | + | self->addBatch(batch, delay); | |
| 580 | + | }); | |
| 581 | + | return; | |
| 582 | + | } | |
| 583 | + | ||
| 584 | + | // Safety check - if batch is null, just return | |
| 585 | + | if (!batch) { | |
| 586 | + | return; | |
| 587 | + | } | |
| 588 | + | ||
| 589 | + | // Additional safety check to catch any potential crashes | |
| 590 | + | try { | |
| 591 | + | ASSERT(current <= actions.size()); | |
| 592 | + | ||
| 593 | + | if (batch->size() == 0) { | |
| 594 | + | delete batch; | |
| 595 | + | return; | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | // Commit any uncommited actions... | |
| 599 | + | batch->commit(); | |
| 600 | + | ||
| 601 | + | // Update title | |
| 602 | + | if (editor.map.doChange()) { | |
| 603 | + | // Use a safer version that doesn't trigger UI updates | |
| 604 | + | // during the first drawing operation | |
| 605 | + | static bool isFirstOperation = true; | |
| 606 | + | if (!isFirstOperation) { | |
| 607 | + | g_gui.UpdateTitle(); | |
| 608 | + | } else { | |
| 609 | + | isFirstOperation = false; | |
| 610 | + | } | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | if (batch->type == ACTION_REMOTE) { | |
| 614 | + | delete batch; | |
| 615 | + | return; | |
| 616 | + | } | |
| 617 | + | ||
| 618 | + | // Protect against memory corruption | |
| 619 | + | while (current < actions.size() && !actions.empty()) { | |
| 620 | + | try { | |
| 621 | + | memory_size -= actions.back()->memsize(); | |
| 622 | + | BatchAction* todelete = actions.back(); | |
| 623 | + | actions.pop_back(); | |
| 624 | + | delete todelete; | |
| 625 | + | } catch (const std::exception& e) { | |
| 626 | + | // Log error but continue processing | |
| 627 | + | std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app); | |
| 628 | + | if (logFile.is_open()) { | |
| 629 | + | wxDateTime now = wxDateTime::Now(); | |
| 630 | + | logFile << now.FormatISOCombined() << ": Error clearing action: " << e.what() << "\n"; | |
| 631 | + | logFile.close(); | |
| 632 | + | } | |
| 633 | + | break; | |
| 634 | + | } | |
| 635 | + | } | |
| 636 | + | ||
| 637 | + | // Safely manage memory | |
| 638 | + | try { | |
| 639 | + | while (memory_size > size_t(1024 * 1024 * g_settings.getInteger(Config::UNDO_MEM_SIZE)) && !actions.empty()) { | |
| 640 | + | memory_size -= actions.front()->memsize(); | |
| 641 | + | delete actions.front(); | |
| 642 | + | actions.pop_front(); | |
| 643 | + | if (current > 0) { | |
| 644 | + | current--; | |
| 645 | + | } | |
| 646 | + | } | |
| 647 | + | ||
| 648 | + | if (actions.size() > size_t(g_settings.getInteger(Config::UNDO_SIZE)) && !actions.empty()) { | |
| 649 | + | memory_size -= actions.front()->memsize(); | |
| 650 | + | BatchAction* todelete = actions.front(); | |
| 651 | + | actions.pop_front(); | |
| 652 | + | delete todelete; | |
| 653 | + | if (current > 0) { | |
| 654 | + | current--; | |
| 655 | + | } | |
| 656 | + | } | |
| 657 | + | ||
| 658 | + | // Process action with additional safety | |
| 659 | + | do { | |
| 660 | + | if (!actions.empty()) { | |
| 661 | + | BatchAction* lastAction = actions.back(); | |
| 662 | + | if (lastAction->type == batch->type && g_settings.getInteger(Config::GROUP_ACTIONS) && time(nullptr) - stacking_delay < lastAction->timestamp) { | |
| 663 | + | lastAction->merge(batch); | |
| 664 | + | lastAction->timestamp = time(nullptr); | |
| 665 | + | memory_size -= lastAction->memsize(); | |
| 666 | + | memory_size += lastAction->memsize(true); | |
| 667 | + | delete batch; | |
| 668 | + | break; | |
| 669 | + | } | |
| 670 | + | } | |
| 671 | + | memory_size += batch->memsize(); | |
| 672 | + | actions.push_back(batch); | |
| 673 | + | batch->timestamp = time(nullptr); | |
| 674 | + | current++; | |
| 675 | + | } while (false); | |
| 676 | + | } catch (const std::exception& e) { | |
| 677 | + | // Log error but don't crash | |
| 678 | + | std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app); | |
| 679 | + | if (logFile.is_open()) { | |
| 680 | + | wxDateTime now = wxDateTime::Now(); | |
| 681 | + | logFile << now.FormatISOCombined() << ": Error processing batch: " << e.what() << "\n"; | |
| 682 | + | logFile.close(); | |
| 683 | + | } | |
| 684 | + | } | |
| 685 | + | } catch (const std::exception& e) { | |
| 686 | + | // Last resort error handler for entire function | |
| 687 | + | std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app); | |
| 688 | + | if (logFile.is_open()) { | |
| 689 | + | wxDateTime now = wxDateTime::Now(); | |
| 690 | + | logFile << now.FormatISOCombined() << ": Critical error in addBatch: " << e.what() << "\n"; | |
| 691 | + | logFile.close(); | |
| 692 | + | } | |
| 693 | + | } | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | void ActionQueue::undo() { | |
| 697 | + | if (current > 0) { | |
| 698 | + | current--; | |
| 699 | + | BatchAction* batch = actions[current]; | |
| 700 | + | batch->undo(); | |
| 701 | + | } | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | void ActionQueue::redo() { | |
| 705 | + | if (current < actions.size()) { | |
| 706 | + | BatchAction* batch = actions[current]; | |
| 707 | + | batch->redo(); | |
| 708 | + | current++; | |
| 709 | + | } | |
| 710 | + | } | |
| 711 | + | ||
| 712 | + | void ActionQueue::clear() { | |
| 713 | + | for (ActionList::iterator it = actions.begin(); it != actions.end();) { | |
| 714 | + | delete *it; | |
| 715 | + | it = actions.erase(it); | |
| 716 | + | } | |
| 717 | + | current = 0; | |
| 718 | + | } | |
| 719 | + | ||
| 720 | + | DirtyList::DirtyList() : | |
| 721 | + | owner(0) { | |
| 722 | + | ; | |
| 723 | + | } | |
| 724 | + | ||
| 725 | + | DirtyList::~DirtyList() { | |
| 726 | + | ; | |
| 727 | + | } | |
| 728 | + | ||
| 729 | + | void DirtyList::AddPosition(int x, int y, int z) { | |
| 730 | + | uint32_t m = ((x >> 2) << 18) | ((y >> 2) << 4); | |
| 731 | + | ValueType fi = { m, 0 }; | |
| 732 | + | SetType::iterator s = iset.find(fi); | |
| 733 | + | if (s != iset.end()) { | |
| 734 | + | ValueType v = *s; | |
| 735 | + | iset.erase(s); | |
| 736 | + | v.floors = (1 << z) | v.floors; | |
| 737 | + | iset.insert(v); | |
| 738 | + | } else { | |
| 739 | + | ValueType v = { m, (uint32_t)(1 << z) }; | |
| 740 | + | iset.insert(v); | |
| 741 | + | } | |
| 742 | + | } | |
| 743 | + | ||
| 744 | + | void DirtyList::AddChange(Change* c) { | |
| 745 | + | ichanges.push_back(c); | |
| 746 | + | } | |
| 747 | + | ||
| 748 | + | DirtyList::SetType& DirtyList::GetPosList() { | |
| 749 | + | return iset; | |
| 750 | + | } | |
| 751 | + | ||
| 752 | + | ChangeList& DirtyList::GetChanges() { | |
| 753 | + | return ichanges; | |
| 754 | + | } |
| @@ -0,0 +1,234 @@ | |||
| 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_ACTION_H_ | |
| 19 | + | #define RME_ACTION_H_ | |
| 20 | + | ||
| 21 | + | #include "position.h" | |
| 22 | + | ||
| 23 | + | #include <deque> | |
| 24 | + | ||
| 25 | + | class Editor; | |
| 26 | + | class Tile; | |
| 27 | + | class House; | |
| 28 | + | class Waypoint; | |
| 29 | + | class Change; | |
| 30 | + | class Action; | |
| 31 | + | class BatchAction; | |
| 32 | + | class ActionQueue; | |
| 33 | + | ||
| 34 | + | enum ChangeType { | |
| 35 | + | CHANGE_NONE, | |
| 36 | + | CHANGE_TILE, | |
| 37 | + | CHANGE_MOVE_HOUSE_EXIT, | |
| 38 | + | CHANGE_MOVE_WAYPOINT, | |
| 39 | + | }; | |
| 40 | + | ||
| 41 | + | class Change { | |
| 42 | + | private: | |
| 43 | + | ChangeType type; | |
| 44 | + | void* data; | |
| 45 | + | ||
| 46 | + | Change(); | |
| 47 | + | ||
| 48 | + | public: | |
| 49 | + | Change(Tile* tile); | |
| 50 | + | static Change* Create(House* house, const Position& where); | |
| 51 | + | static Change* Create(Waypoint* wp, const Position& where); | |
| 52 | + | ~Change(); | |
| 53 | + | void clear(); | |
| 54 | + | ||
| 55 | + | ChangeType getType() const { | |
| 56 | + | return type; | |
| 57 | + | } | |
| 58 | + | void* getData() const { | |
| 59 | + | return data; | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Get memory footprint | |
| 63 | + | uint32_t memsize() const; | |
| 64 | + | ||
| 65 | + | friend class Action; | |
| 66 | + | }; | |
| 67 | + | ||
| 68 | + | typedef std::vector<Change*> ChangeList; | |
| 69 | + | ||
| 70 | + | // A dirty list represents a list of all tiles that was changed in an action | |
| 71 | + | class DirtyList { | |
| 72 | + | public: | |
| 73 | + | DirtyList(); | |
| 74 | + | ~DirtyList(); | |
| 75 | + | ||
| 76 | + | struct ValueType { | |
| 77 | + | uint32_t pos; | |
| 78 | + | uint32_t floors; | |
| 79 | + | }; | |
| 80 | + | ||
| 81 | + | uint32_t owner; | |
| 82 | + | ||
| 83 | + | protected: | |
| 84 | + | struct Comparator { | |
| 85 | + | bool operator()(const ValueType& a, const ValueType& b) const { | |
| 86 | + | return a.pos < b.pos; | |
| 87 | + | } | |
| 88 | + | }; | |
| 89 | + | ||
| 90 | + | public: | |
| 91 | + | typedef std::set<ValueType, Comparator> SetType; | |
| 92 | + | ||
| 93 | + | void AddPosition(int x, int y, int z); | |
| 94 | + | void AddChange(Change* c); | |
| 95 | + | bool Empty() const { | |
| 96 | + | return iset.empty() && ichanges.empty(); | |
| 97 | + | } | |
| 98 | + | SetType& GetPosList(); | |
| 99 | + | ChangeList& GetChanges(); | |
| 100 | + | ||
| 101 | + | protected: | |
| 102 | + | SetType iset; | |
| 103 | + | ChangeList ichanges; | |
| 104 | + | }; | |
| 105 | + | ||
| 106 | + | enum ActionIdentifier { | |
| 107 | + | ACTION_MOVE, | |
| 108 | + | ACTION_REMOTE, | |
| 109 | + | ACTION_SELECT, | |
| 110 | + | ACTION_DELETE_TILES, | |
| 111 | + | ACTION_CUT_TILES, | |
| 112 | + | ACTION_PASTE_TILES, | |
| 113 | + | ACTION_RANDOMIZE, | |
| 114 | + | ACTION_BORDERIZE, | |
| 115 | + | ACTION_DRAW, | |
| 116 | + | ACTION_SWITCHDOOR, | |
| 117 | + | ACTION_ROTATE_ITEM, | |
| 118 | + | ACTION_REPLACE_ITEMS, | |
| 119 | + | ACTION_CHANGE_PROPERTIES, | |
| 120 | + | }; | |
| 121 | + | ||
| 122 | + | class Action { | |
| 123 | + | public: | |
| 124 | + | virtual ~Action(); | |
| 125 | + | ||
| 126 | + | void addChange(Change* t) { | |
| 127 | + | changes.push_back(t); | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // Get memory footprint | |
| 131 | + | size_t approx_memsize() const; | |
| 132 | + | size_t memsize() const; | |
| 133 | + | size_t size() const { | |
| 134 | + | return changes.size(); | |
| 135 | + | } | |
| 136 | + | ActionIdentifier getType() const { | |
| 137 | + | return type; | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | void commit(DirtyList* dirty_list); | |
| 141 | + | bool isCommited() const { | |
| 142 | + | return commited; | |
| 143 | + | } | |
| 144 | + | void undo(DirtyList* dirty_list); | |
| 145 | + | void redo(DirtyList* dirty_list) { | |
| 146 | + | commit(dirty_list); | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | protected: | |
| 150 | + | Action(Editor& editor, ActionIdentifier ident); | |
| 151 | + | ||
| 152 | + | bool commited; | |
| 153 | + | ChangeList changes; | |
| 154 | + | Editor& editor; | |
| 155 | + | ActionIdentifier type; | |
| 156 | + | ||
| 157 | + | friend class ActionQueue; | |
| 158 | + | }; | |
| 159 | + | ||
| 160 | + | typedef std::vector<Action*> ActionVector; | |
| 161 | + | ||
| 162 | + | class BatchAction { | |
| 163 | + | public: | |
| 164 | + | virtual ~BatchAction(); | |
| 165 | + | ||
| 166 | + | void resetTimer() { | |
| 167 | + | timestamp = 0; | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // Get memory footprint | |
| 171 | + | size_t memsize(bool resize = false) const; | |
| 172 | + | size_t size() const { | |
| 173 | + | return batch.size(); | |
| 174 | + | } | |
| 175 | + | ActionIdentifier getType() const { | |
| 176 | + | return type; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | virtual void addAction(Action* action); | |
| 180 | + | virtual void addAndCommitAction(Action* action); | |
| 181 | + | ||
| 182 | + | protected: | |
| 183 | + | BatchAction(Editor& editor, ActionIdentifier ident); | |
| 184 | + | ||
| 185 | + | virtual void commit(); | |
| 186 | + | virtual void undo(); | |
| 187 | + | virtual void redo(); | |
| 188 | + | ||
| 189 | + | void merge(BatchAction* other); | |
| 190 | + | ||
| 191 | + | Editor& editor; | |
| 192 | + | int timestamp; | |
| 193 | + | uint32_t memory_size; | |
| 194 | + | ActionIdentifier type; | |
| 195 | + | ActionVector batch; | |
| 196 | + | ||
| 197 | + | friend class ActionQueue; | |
| 198 | + | }; | |
| 199 | + | ||
| 200 | + | class ActionQueue { | |
| 201 | + | public: | |
| 202 | + | ActionQueue(Editor& editor); | |
| 203 | + | virtual ~ActionQueue(); | |
| 204 | + | ||
| 205 | + | typedef std::deque<BatchAction*> ActionList; | |
| 206 | + | ||
| 207 | + | void resetTimer(); | |
| 208 | + | ||
| 209 | + | virtual Action* createAction(ActionIdentifier ident); | |
| 210 | + | virtual Action* createAction(BatchAction* parent); | |
| 211 | + | virtual BatchAction* createBatch(ActionIdentifier ident); | |
| 212 | + | ||
| 213 | + | void addBatch(BatchAction* action, int stacking_delay = 0); | |
| 214 | + | void addAction(Action* action, int stacking_delay = 0); | |
| 215 | + | ||
| 216 | + | void undo(); | |
| 217 | + | void redo(); | |
| 218 | + | void clear(); | |
| 219 | + | ||
| 220 | + | bool canUndo() { | |
| 221 | + | return current > 0; | |
| 222 | + | } | |
| 223 | + | bool canRedo() { | |
| 224 | + | return current < actions.size(); | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | protected: | |
| 228 | + | size_t current; | |
| 229 | + | size_t memory_size; | |
| 230 | + | Editor& editor; | |
| 231 | + | ActionList actions; | |
| 232 | + | }; | |
| 233 | + | ||
| 234 | + | #endif |
| @@ -0,0 +1,129 @@ | |||
| 1 | + | #include "main.h" | |
| 2 | + | #include "add_creature_dialog.h" | |
| 3 | + | #include "materials.h" | |
| 4 | + | #include "creature_brush.h" | |
| 5 | + | ||
| 6 | + | #include <wx/spinctrl.h> | |
| 7 | + | #include <wx/statbox.h> | |
| 8 | + | #include <wx/sizer.h> | |
| 9 | + | #include <wx/stattext.h> | |
| 10 | + | #include <wx/textctrl.h> | |
| 11 | + | #include <wx/radiobox.h> | |
| 12 | + | ||
| 13 | + | BEGIN_EVENT_TABLE(AddCreatureDialog, wxDialog) | |
| 14 | + | EVT_BUTTON(wxID_OK, AddCreatureDialog::OnClickOK) | |
| 15 | + | EVT_BUTTON(wxID_CANCEL, AddCreatureDialog::OnClickCancel) | |
| 16 | + | END_EVENT_TABLE() | |
| 17 | + | ||
| 18 | + | AddCreatureDialog::AddCreatureDialog(wxWindow* parent, const wxString& name) : | |
| 19 | + | wxDialog(parent, wxID_ANY, "Add New Creature", wxDefaultPosition, wxDefaultSize), | |
| 20 | + | creature_type(nullptr) | |
| 21 | + | { | |
| 22 | + | // Create sizers | |
| 23 | + | wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); | |
| 24 | + | wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); | |
| 25 | + | ||
| 26 | + | // Create form fields | |
| 27 | + | wxStaticBoxSizer* name_box = new wxStaticBoxSizer(wxVERTICAL, this, "Name"); | |
| 28 | + | name_field = new wxTextCtrl(this, wxID_ANY, name); | |
| 29 | + | name_box->Add(name_field, 0, wxEXPAND | wxALL, 5); | |
| 30 | + | form_sizer->Add(name_box, 0, wxEXPAND | wxALL, 5); | |
| 31 | + | ||
| 32 | + | // Type selection | |
| 33 | + | wxArrayString type_choices; | |
| 34 | + | type_choices.Add("Monster"); | |
| 35 | + | type_choices.Add("NPC"); | |
| 36 | + | type_radio = new wxRadioBox(this, wxID_ANY, "Creature Type", | |
| 37 | + | wxDefaultPosition, wxDefaultSize, | |
| 38 | + | type_choices, 1, wxRA_SPECIFY_ROWS); | |
| 39 | + | form_sizer->Add(type_radio, 0, wxEXPAND | wxALL, 5); | |
| 40 | + | ||
| 41 | + | // Outfit settings | |
| 42 | + | wxStaticBoxSizer* outfit_box = new wxStaticBoxSizer(wxVERTICAL, this, "Outfit"); | |
| 43 | + | wxFlexGridSizer* outfit_sizer = new wxFlexGridSizer(2, 5, 5); | |
| 44 | + | ||
| 45 | + | outfit_sizer->Add(new wxStaticText(this, wxID_ANY, "Look Type:")); | |
| 46 | + | looktype_field = new wxSpinCtrl(this, wxID_ANY, "1", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 1500, 1); | |
| 47 | + | outfit_sizer->Add(looktype_field); | |
| 48 | + | ||
| 49 | + | outfit_sizer->Add(new wxStaticText(this, wxID_ANY, "Head:")); | |
| 50 | + | lookhead_field = new wxSpinCtrl(this, wxID_ANY, "0", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255, 0); | |
| 51 | + | outfit_sizer->Add(lookhead_field); | |
| 52 | + | ||
| 53 | + | outfit_sizer->Add(new wxStaticText(this, wxID_ANY, "Body:")); | |
| 54 | + | lookbody_field = new wxSpinCtrl(this, wxID_ANY, "0", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255, 0); | |
| 55 | + | outfit_sizer->Add(lookbody_field); | |
| 56 | + | ||
| 57 | + | outfit_sizer->Add(new wxStaticText(this, wxID_ANY, "Legs:")); | |
| 58 | + | looklegs_field = new wxSpinCtrl(this, wxID_ANY, "0", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255, 0); | |
| 59 | + | outfit_sizer->Add(looklegs_field); | |
| 60 | + | ||
| 61 | + | outfit_sizer->Add(new wxStaticText(this, wxID_ANY, "Feet:")); | |
| 62 | + | lookfeet_field = new wxSpinCtrl(this, wxID_ANY, "0", wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 255, 0); | |
| 63 | + | outfit_sizer->Add(lookfeet_field); | |
| 64 | + | ||
| 65 | + | outfit_box->Add(outfit_sizer, 0, wxEXPAND | wxALL, 5); | |
| 66 | + | form_sizer->Add(outfit_box, 0, wxEXPAND | wxALL, 5); | |
| 67 | + | ||
| 68 | + | // Add buttons | |
| 69 | + | wxSizer* button_sizer = CreateButtonSizer(wxOK | wxCANCEL); | |
| 70 | + | ||
| 71 | + | // Add everything to the dialog | |
| 72 | + | topsizer->Add(form_sizer, 1, wxEXPAND | wxALL, 5); | |
| 73 | + | topsizer->Add(button_sizer, 0, wxEXPAND | wxALL, 5); | |
| 74 | + | ||
| 75 | + | SetSizerAndFit(topsizer); | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | AddCreatureDialog::~AddCreatureDialog() | |
| 79 | + | { | |
| 80 | + | // Clean up if needed | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | void AddCreatureDialog::OnClickOK(wxCommandEvent& event) | |
| 84 | + | { | |
| 85 | + | // Get values from form | |
| 86 | + | std::string name = name_field->GetValue().ToStdString(); | |
| 87 | + | bool isNpc = (type_radio->GetSelection() == 1); | |
| 88 | + | ||
| 89 | + | // Create outfit | |
| 90 | + | Outfit outfit; | |
| 91 | + | outfit.lookType = looktype_field->GetValue(); | |
| 92 | + | outfit.lookHead = lookhead_field->GetValue(); | |
| 93 | + | outfit.lookBody = lookbody_field->GetValue(); | |
| 94 | + | outfit.lookLegs = looklegs_field->GetValue(); | |
| 95 | + | outfit.lookFeet = lookfeet_field->GetValue(); | |
| 96 | + | ||
| 97 | + | // Create new creature type | |
| 98 | + | creature_type = g_creatures.addCreatureType(name, isNpc, outfit); | |
| 99 | + | ||
| 100 | + | if (creature_type) { | |
| 101 | + | // Create brush and add to appropriate tileset | |
| 102 | + | Tileset* tileset = nullptr; | |
| 103 | + | if (isNpc) { | |
| 104 | + | tileset = g_materials.tilesets["NPCs"]; | |
| 105 | + | } else { | |
| 106 | + | tileset = g_materials.tilesets["Others"]; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | if (tileset) { | |
| 110 | + | Brush* brush = newd CreatureBrush(creature_type); | |
| 111 | + | g_brushes.addBrush(brush); | |
| 112 | + | ||
| 113 | + | TilesetCategory* category = tileset->getCategory(TILESET_CREATURE); | |
| 114 | + | if (category) { | |
| 115 | + | category->brushlist.push_back(brush); | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | EndModal(wxID_OK); | |
| 120 | + | } else { | |
| 121 | + | wxMessageBox("Failed to create creature. The name might already be in use.", | |
| 122 | + | "Error", wxOK | wxICON_ERROR, this); | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | void AddCreatureDialog::OnClickCancel(wxCommandEvent& event) | |
| 127 | + | { | |
| 128 | + | EndModal(wxID_CANCEL); | |
| 129 | + | } |
| @@ -0,0 +1,36 @@ | |||
| 1 | + | #ifndef RME_ADD_CREATURE_DIALOG_H_ | |
| 2 | + | #define RME_ADD_CREATURE_DIALOG_H_ | |
| 3 | + | ||
| 4 | + | #include <wx/dialog.h> | |
| 5 | + | #include "creatures.h" | |
| 6 | + | ||
| 7 | + | class wxSpinCtrl; | |
| 8 | + | class wxTextCtrl; | |
| 9 | + | class wxRadioBox; | |
| 10 | + | ||
| 11 | + | class AddCreatureDialog : public wxDialog { | |
| 12 | + | public: | |
| 13 | + | AddCreatureDialog(wxWindow* parent, const wxString& name); | |
| 14 | + | virtual ~AddCreatureDialog(); | |
| 15 | + | ||
| 16 | + | CreatureType* GetCreatureType() const { return creature_type; } | |
| 17 | + | ||
| 18 | + | protected: | |
| 19 | + | void OnClickOK(wxCommandEvent& event); | |
| 20 | + | void OnClickCancel(wxCommandEvent& event); | |
| 21 | + | ||
| 22 | + | private: | |
| 23 | + | wxTextCtrl* name_field; | |
| 24 | + | wxSpinCtrl* looktype_field; | |
| 25 | + | wxSpinCtrl* lookhead_field; | |
| 26 | + | wxSpinCtrl* lookbody_field; | |
| 27 | + | wxSpinCtrl* looklegs_field; | |
| 28 | + | wxSpinCtrl* lookfeet_field; | |
| 29 | + | wxRadioBox* type_radio; | |
| 30 | + | ||
| 31 | + | CreatureType* creature_type; | |
| 32 | + | ||
| 33 | + | DECLARE_EVENT_TABLE() | |
| 34 | + | }; | |
| 35 | + | ||
| 36 | + | #endif // RME_ADD_CREATURE_DIALOG_H_ |
| @@ -0,0 +1,441 @@ | |||
| 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/grid.h> | |
| 21 | + | #include <wx/choice.h> | |
| 22 | + | #include <wx/spinctrl.h> | |
| 23 | + | ||
| 24 | + | #include "tile.h" | |
| 25 | + | #include "item.h" | |
| 26 | + | #include "complexitem.h" | |
| 27 | + | #include "town.h" | |
| 28 | + | #include "house.h" | |
| 29 | + | #include "map.h" | |
| 30 | + | #include "editor.h" | |
| 31 | + | #include "creature.h" | |
| 32 | + | #include "materials.h" | |
| 33 | + | #include "tileset.h" | |
| 34 | + | ||
| 35 | + | #include "gui.h" | |
| 36 | + | #include "application.h" | |
| 37 | + | #include "add_item_window.h" | |
| 38 | + | #include "container_properties_window.h" | |
| 39 | + | #include "find_item_window.h" | |
| 40 | + | #include "palette_window.h" | |
| 41 | + | #include "palette_brushlist.h" | |
| 42 | + | #include "brush.h" | |
| 43 | + | #include "raw_brush.h" | |
| 44 | + | ||
| 45 | + | // Forward declarations | |
| 46 | + | class PaletteWindow; | |
| 47 | + | class BrushPalettePanel; | |
| 48 | + | ||
| 49 | + | // ============================================================================ | |
| 50 | + | // Add Item Window | |
| 51 | + | ||
| 52 | + | static constexpr int OUTFIT_COLOR_MAX = 133; | |
| 53 | + | ||
| 54 | + | BEGIN_EVENT_TABLE(AddItemWindow, wxDialog) | |
| 55 | + | EVT_BUTTON(wxID_OK, AddItemWindow::OnClickOK) | |
| 56 | + | EVT_BUTTON(wxID_CANCEL, AddItemWindow::OnClickCancel) | |
| 57 | + | EVT_CHECKBOX(wxID_ANY, AddItemWindow::OnRangeToggle) | |
| 58 | + | EVT_SPINCTRL(wxID_ANY, AddItemWindow::OnRangeFieldChange) | |
| 59 | + | END_EVENT_TABLE() | |
| 60 | + | ||
| 61 | + | AddItemWindow::AddItemWindow(wxWindow* win_parent, TilesetCategoryType categoryType, Tileset* tilesetItem, wxPoint pos) : | |
| 62 | + | ObjectPropertiesWindowBase(win_parent, "Add a Item", pos), | |
| 63 | + | item_id(0), | |
| 64 | + | tileset_item(tilesetItem), | |
| 65 | + | category_type(categoryType), | |
| 66 | + | item_id_field(nullptr), | |
| 67 | + | item_id_label(nullptr), | |
| 68 | + | item_name_label(nullptr), | |
| 69 | + | item_button(nullptr), | |
| 70 | + | tileset_choice(nullptr), | |
| 71 | + | range_checkbox(nullptr), | |
| 72 | + | range_start_field(nullptr), | |
| 73 | + | range_end_field(nullptr), | |
| 74 | + | range_start_label(nullptr), | |
| 75 | + | range_end_label(nullptr), | |
| 76 | + | range_info_label(nullptr) { | |
| 77 | + | wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL); | |
| 78 | + | wxString description = "Add a Item"; | |
| 79 | + | ||
| 80 | + | wxSizer* boxsizer = newd wxStaticBoxSizer(wxVERTICAL, this, description); | |
| 81 | + | ||
| 82 | + | wxFlexGridSizer* subsizer = newd wxFlexGridSizer(2, 10, 10); | |
| 83 | + | subsizer->AddGrowableCol(1); | |
| 84 | + | ||
| 85 | + | uint16_t itemId = 0; | |
| 86 | + | std::string itemName = "None"; | |
| 87 | + | item_id_label = newd wxStaticText(this, wxID_ANY, "ID " + i2ws(itemId)); | |
| 88 | + | subsizer->Add(item_id_label); | |
| 89 | + | item_name_label = newd wxStaticText(this, wxID_ANY, "\"" + wxstr(itemName) + "\""); | |
| 90 | + | subsizer->Add(item_name_label); | |
| 91 | + | ||
| 92 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Item"), wxSizerFlags(1).CenterVertical()); | |
| 93 | + | item_button = newd DCButton(this, wxID_ANY, wxDefaultPosition, DC_BTN_TOGGLE, RENDER_SIZE_32x32, 0); | |
| 94 | + | subsizer->Add(item_button); | |
| 95 | + | ||
| 96 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Item Id")); | |
| 97 | + | item_id_field = newd wxSpinCtrl(this, wxID_ANY, i2ws(itemId), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 100, 100000); | |
| 98 | + | item_id_field->Connect(wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler(AddItemWindow::OnChangeItemId), NULL, this); | |
| 99 | + | subsizer->Add(item_id_field, wxSizerFlags(1).Expand()); | |
| 100 | + | ||
| 101 | + | // Add range selection checkbox | |
| 102 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Add multiple items")); | |
| 103 | + | wxBoxSizer* range_checkbox_sizer = newd wxBoxSizer(wxHORIZONTAL); | |
| 104 | + | range_checkbox = newd wxCheckBox(this, wxID_ANY, "Add a range of items"); | |
| 105 | + | range_checkbox_sizer->Add(range_checkbox, 1, wxEXPAND | wxRIGHT, 5); | |
| 106 | + | ||
| 107 | + | // Add a "Quick Range" button | |
| 108 | + | wxButton* quick_range_button = newd wxButton(this, wxID_ANY, "Quick +10 Range", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); | |
| 109 | + | quick_range_button->Connect(wxEVT_BUTTON, wxCommandEventHandler(AddItemWindow::OnQuickRange), NULL, this); | |
| 110 | + | quick_range_button->SetToolTip("Quickly set up a range from current ID to current ID + 10"); | |
| 111 | + | range_checkbox_sizer->Add(quick_range_button, 0, wxALIGN_CENTER_VERTICAL); | |
| 112 | + | ||
| 113 | + | subsizer->Add(range_checkbox_sizer, wxSizerFlags(1).Expand()); | |
| 114 | + | ||
| 115 | + | // Add range selection fields (initially hidden) | |
| 116 | + | range_start_label = newd wxStaticText(this, wxID_ANY, "Range start"); | |
| 117 | + | subsizer->Add(range_start_label); | |
| 118 | + | range_start_field = newd wxSpinCtrl(this, wxID_ANY, i2ws(itemId), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 100, 100000); | |
| 119 | + | range_start_field->Connect(wxEVT_SPINCTRL, wxSpinEventHandler(AddItemWindow::OnRangeFieldChange), NULL, this); | |
| 120 | + | subsizer->Add(range_start_field, wxSizerFlags(1).Expand()); | |
| 121 | + | ||
| 122 | + | range_end_label = newd wxStaticText(this, wxID_ANY, "Range end"); | |
| 123 | + | subsizer->Add(range_end_label); | |
| 124 | + | ||
| 125 | + | // Create a horizontal sizer for the end field and button | |
| 126 | + | wxBoxSizer* end_field_sizer = newd wxBoxSizer(wxHORIZONTAL); | |
| 127 | + | range_end_field = newd wxSpinCtrl(this, wxID_ANY, i2ws(itemId), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 100, 100000); | |
| 128 | + | range_end_field->Connect(wxEVT_SPINCTRL, wxSpinEventHandler(AddItemWindow::OnRangeFieldChange), NULL, this); | |
| 129 | + | end_field_sizer->Add(range_end_field, 1, wxEXPAND | wxRIGHT, 5); | |
| 130 | + | ||
| 131 | + | // Add a "Use Current" button | |
| 132 | + | wxButton* use_current_button = newd wxButton(this, wxID_ANY, "Use Current Item", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); | |
| 133 | + | use_current_button->Connect(wxEVT_BUTTON, wxCommandEventHandler(AddItemWindow::OnUseCurrentItem), NULL, this); | |
| 134 | + | end_field_sizer->Add(use_current_button, 0, wxALIGN_CENTER_VERTICAL); | |
| 135 | + | ||
| 136 | + | subsizer->Add(end_field_sizer, wxSizerFlags(1).Expand()); | |
| 137 | + | ||
| 138 | + | // Info label | |
| 139 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Status")); | |
| 140 | + | range_info_label = newd wxStaticText(this, wxID_ANY, ""); | |
| 141 | + | subsizer->Add(range_info_label); | |
| 142 | + | ||
| 143 | + | // Initially hide range controls | |
| 144 | + | UpdateRangeFields(false); | |
| 145 | + | ||
| 146 | + | // Add tileset selection dropdown | |
| 147 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Tileset")); | |
| 148 | + | tileset_choice = newd wxChoice(this, wxID_ANY); | |
| 149 | + | ||
| 150 | + | // Populate the choice with all available tilesets | |
| 151 | + | int currentSelection = 0; | |
| 152 | + | int index = 0; | |
| 153 | + | for (TilesetContainer::iterator iter = g_materials.tilesets.begin(); iter != g_materials.tilesets.end(); ++iter) { | |
| 154 | + | if (iter->second->getCategory(category_type)) { | |
| 155 | + | tileset_choice->Append(wxstr(iter->first)); | |
| 156 | + | // Set current selection to the provided tileset if it matches | |
| 157 | + | if (tileset_item && iter->second == tileset_item) { | |
| 158 | + | currentSelection = index; | |
| 159 | + | } | |
| 160 | + | index++; | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // Select the current tileset if available | |
| 165 | + | if (tileset_choice->GetCount() > 0) { | |
| 166 | + | tileset_choice->SetSelection(currentSelection); | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | subsizer->Add(tileset_choice, wxSizerFlags(1).Expand()); | |
| 170 | + | ||
| 171 | + | // Add "Quick Selection Range" button that will use selected brushes from palette | |
| 172 | + | wxButton* quick_selection_button = newd wxButton(this, wxID_ANY, "Add Selection from Palette", | |
| 173 | + | wxDefaultPosition, wxDefaultSize, 0); | |
| 174 | + | quick_selection_button->Connect(wxEVT_BUTTON, wxCommandEventHandler(AddItemWindow::OnQuickRange), NULL, this); | |
| 175 | + | quick_selection_button->SetToolTip("Add all items currently selected in the palette to the tileset"); | |
| 176 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Quick Add")); | |
| 177 | + | subsizer->Add(quick_selection_button, wxSizerFlags(1).Expand()); | |
| 178 | + | ||
| 179 | + | boxsizer->Add(subsizer, wxSizerFlags(1).Expand()); | |
| 180 | + | ||
| 181 | + | topsizer->Add(boxsizer, wxSizerFlags(0).Expand().Border(wxLEFT | wxRIGHT, 20)); | |
| 182 | + | ||
| 183 | + | wxSizer* subsizer_ = newd wxBoxSizer(wxHORIZONTAL); | |
| 184 | + | subsizer_->Add(newd wxButton(this, wxID_OK, "Add"), wxSizerFlags(1).Center().Border(wxTOP | wxBOTTOM, 10)); | |
| 185 | + | subsizer_->Add(newd wxButton(this, wxID_CANCEL, "Cancel"), wxSizerFlags(1).Center().Border(wxTOP | wxBOTTOM, 10)); | |
| 186 | + | topsizer->Add(subsizer_, wxSizerFlags(0).Center().Border(wxLEFT | wxRIGHT, 20)); | |
| 187 | + | ||
| 188 | + | SetSizerAndFit(topsizer); | |
| 189 | + | Centre(wxBOTH); | |
| 190 | + | ||
| 191 | + | item_button->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(AddItemWindow::OnItemClicked), NULL, this); | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | void AddItemWindow::OnClickOK(wxCommandEvent& WXUNUSED(event)) { | |
| 195 | + | // Get the selected tileset name from the dropdown | |
| 196 | + | wxString tilesetName; | |
| 197 | + | if (tileset_choice && tileset_choice->GetSelection() != wxNOT_FOUND) { | |
| 198 | + | tilesetName = tileset_choice->GetString(tileset_choice->GetSelection()); | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | // If no tileset is selected, use the initial tileset | |
| 202 | + | if (tilesetName.IsEmpty() && tileset_item) { | |
| 203 | + | tilesetName = wxstr(tileset_item->name); | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | // Make sure we have a valid tileset name | |
| 207 | + | if (tilesetName.IsEmpty()) { | |
| 208 | + | g_gui.PopupDialog("Error", "No tileset selected", wxOK); | |
| 209 | + | return; | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | std::string stdTilesetName = nstr(tilesetName); | |
| 213 | + | ||
| 214 | + | // Check if we're in range mode | |
| 215 | + | if (range_checkbox && range_checkbox->GetValue() && | |
| 216 | + | range_start_field && range_end_field) { | |
| 217 | + | ||
| 218 | + | int start = range_start_field->GetValue(); | |
| 219 | + | int end = range_end_field->GetValue(); | |
| 220 | + | ||
| 221 | + | // Validate range | |
| 222 | + | if (start > end) { | |
| 223 | + | g_gui.PopupDialog("Error", "Invalid range: start ID is greater than end ID", wxOK); | |
| 224 | + | return; | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // Add all items in range | |
| 228 | + | int added_count = 0; | |
| 229 | + | for (int id = start; id <= end; id++) { | |
| 230 | + | const ItemType& it = g_items.getItemType(id); | |
| 231 | + | if (it.id != 0) { | |
| 232 | + | g_materials.addToTileset(stdTilesetName, it.id, category_type); | |
| 233 | + | added_count++; | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Save changes | |
| 238 | + | g_materials.modify(); | |
| 239 | + | ||
| 240 | + | // Show success message | |
| 241 | + | if (added_count > 0) { | |
| 242 | + | g_gui.PopupDialog("Items Added", | |
| 243 | + | wxString::Format("Successfully added %d items (IDs %d-%d) to tileset '%s'", | |
| 244 | + | added_count, start, end, tilesetName), wxOK); | |
| 245 | + | EndModal(1); | |
| 246 | + | } else { | |
| 247 | + | g_gui.PopupDialog("Error", "No valid items found in the specified range", wxOK); | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | // Single item mode | |
| 251 | + | else { | |
| 252 | + | const ItemType& it = g_items.getItemType(item_id_field->GetValue()); | |
| 253 | + | if (it.id != 0) { | |
| 254 | + | g_materials.addToTileset(stdTilesetName, it.id, category_type); | |
| 255 | + | g_materials.modify(); | |
| 256 | + | g_gui.PopupDialog("Item Added", | |
| 257 | + | wxString::Format("'%s' (ID: %d) has been added to tileset '%s'", | |
| 258 | + | wxString(it.name.c_str()), it.id, tilesetName), wxOK); | |
| 259 | + | EndModal(1); | |
| 260 | + | } else { | |
| 261 | + | g_gui.PopupDialog("Error", "You need to select an item", wxOK); | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | void AddItemWindow::OnClickCancel(wxCommandEvent& WXUNUSED(event)) { | |
| 267 | + | // Just close this window | |
| 268 | + | EndModal(0); | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | void AddItemWindow::OnChangeItemId(wxCommandEvent& WXUNUSED(event)) { | |
| 272 | + | uint16_t itemId = item_id_field->GetValue(); | |
| 273 | + | ItemType& it = g_items[itemId]; | |
| 274 | + | if (it.id != 0) { | |
| 275 | + | item_id_label->SetLabelText("ID " + i2ws(it.id)); | |
| 276 | + | item_name_label->SetLabelText("\"" + wxstr(it.name) + "\""); | |
| 277 | + | ||
| 278 | + | item_button->SetSprite(it.clientID); | |
| 279 | + | ||
| 280 | + | // Update range start field if range mode is enabled | |
| 281 | + | if (range_checkbox && range_checkbox->GetValue() && range_start_field) { | |
| 282 | + | range_start_field->SetValue(itemId); | |
| 283 | + | UpdateRangeInfo(); | |
| 284 | + | } | |
| 285 | + | } else { | |
| 286 | + | item_id_field->SetValue(100); | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | void AddItemWindow::OnItemClicked(wxMouseEvent& WXUNUSED(event)) { | |
| 291 | + | // Get the GUI instance to access the current palette and selected brush | |
| 292 | + | GUI* gui = &g_gui; | |
| 293 | + | if (!gui) return; | |
| 294 | + | ||
| 295 | + | // Get the selected brush from the current palette | |
| 296 | + | Brush* brush = gui->GetCurrentBrush(); | |
| 297 | + | if (brush) { | |
| 298 | + | uint16_t id = 0; | |
| 299 | + | ||
| 300 | + | // Try to get the ID from the brush | |
| 301 | + | if (brush->isRaw()) { | |
| 302 | + | RAWBrush* raw_brush = brush->asRaw(); | |
| 303 | + | if (raw_brush) { | |
| 304 | + | id = raw_brush->getItemID(); | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // If we got a valid ID, set it | |
| 309 | + | if (id > 0) { | |
| 310 | + | SetItemIdToItemButton(id); | |
| 311 | + | return; | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // Fall back to the FindItemDialog if needed | |
| 316 | + | FindItemDialog dialog(this, "Item"); | |
| 317 | + | if (dialog.ShowModal() == wxID_OK) { | |
| 318 | + | uint16_t id = dialog.getResultID(); | |
| 319 | + | SetItemIdToItemButton(id); | |
| 320 | + | } | |
| 321 | + | dialog.Destroy(); | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | void AddItemWindow::SetItemIdToItemButton(uint16_t id) { | |
| 325 | + | if (!item_button) { | |
| 326 | + | return; | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | if (id != 0) { | |
| 330 | + | const ItemType& it = g_items.getItemType(id); | |
| 331 | + | if (it.id != 0) { | |
| 332 | + | item_id_field->SetValue(it.id); | |
| 333 | + | item_id_label->SetLabelText("ID " + i2ws(it.id)); | |
| 334 | + | item_name_label->SetLabelText("\"" + wxstr(it.name) + "\""); | |
| 335 | + | ||
| 336 | + | item_button->SetSprite(it.clientID); | |
| 337 | + | return; | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | item_button->SetSprite(0); | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | void AddItemWindow::UpdateRangeFields(bool show) { | |
| 345 | + | if (range_start_label) range_start_label->Show(show); | |
| 346 | + | if (range_start_field) range_start_field->Show(show); | |
| 347 | + | if (range_end_label) range_end_label->Show(show); | |
| 348 | + | if (range_end_field) range_end_field->Show(show); | |
| 349 | + | if (range_info_label) range_info_label->Show(show); | |
| 350 | + | ||
| 351 | + | // Set range fields values if showing | |
| 352 | + | if (show && item_id_field) { | |
| 353 | + | int current_id = item_id_field->GetValue(); | |
| 354 | + | if (range_start_field) range_start_field->SetValue(current_id); | |
| 355 | + | if (range_end_field) range_end_field->SetValue(current_id + 10); // Default to 10 item range | |
| 356 | + | ||
| 357 | + | // Update info label | |
| 358 | + | UpdateRangeInfo(); | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | Layout(); | |
| 362 | + | Fit(); | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | void AddItemWindow::UpdateRangeInfo() { | |
| 366 | + | if (!range_info_label || !range_start_field || !range_end_field) | |
| 367 | + | return; | |
| 368 | + | ||
| 369 | + | int start = range_start_field->GetValue(); | |
| 370 | + | int end = range_end_field->GetValue(); | |
| 371 | + | ||
| 372 | + | if (start > end) { | |
| 373 | + | range_info_label->SetLabel(wxString::Format("Invalid range (start > end)")); | |
| 374 | + | range_info_label->SetForegroundColour(*wxRED); | |
| 375 | + | } else { | |
| 376 | + | int count = end - start + 1; | |
| 377 | + | range_info_label->SetLabel(wxString::Format("%d items will be added", count)); | |
| 378 | + | range_info_label->SetForegroundColour(*wxBLACK); | |
| 379 | + | } | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | void AddItemWindow::OnRangeToggle(wxCommandEvent& event) { | |
| 383 | + | if (range_checkbox) { | |
| 384 | + | bool checked = range_checkbox->GetValue(); | |
| 385 | + | UpdateRangeFields(checked); | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | void AddItemWindow::OnRangeFieldChange(wxSpinEvent& event) { | |
| 390 | + | UpdateRangeInfo(); | |
| 391 | + | ||
| 392 | + | // If the event is from the end field, update the sprite preview | |
| 393 | + | if (event.GetId() == range_end_field->GetId()) { | |
| 394 | + | uint16_t id = range_end_field->GetValue(); | |
| 395 | + | const ItemType& it = g_items.getItemType(id); | |
| 396 | + | if (it.id != 0) { | |
| 397 | + | item_id_field->SetValue(id); | |
| 398 | + | item_id_label->SetLabelText("ID " + i2ws(it.id)); | |
| 399 | + | item_name_label->SetLabelText("\"" + wxstr(it.name) + "\""); | |
| 400 | + | item_button->SetSprite(it.clientID); | |
| 401 | + | } | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | void AddItemWindow::OnUseCurrentItem(wxCommandEvent& event) { | |
| 406 | + | // Get the current item ID from the field instead of the button | |
| 407 | + | if (item_id_field) { | |
| 408 | + | uint16_t id = item_id_field->GetValue(); | |
| 409 | + | if (id != 0) { | |
| 410 | + | range_end_field->SetValue(id); | |
| 411 | + | UpdateRangeInfo(); | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | void AddItemWindow::OnQuickRange(wxCommandEvent& WXUNUSED(event)) { | |
| 417 | + | // For now, just set a range of +10 items from the current item ID | |
| 418 | + | if (item_id_field) { | |
| 419 | + | uint16_t id = item_id_field->GetValue(); | |
| 420 | + | if (id != 0) { | |
| 421 | + | // Enable range mode | |
| 422 | + | if (range_checkbox) { | |
| 423 | + | range_checkbox->SetValue(true); | |
| 424 | + | UpdateRangeFields(true); | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // Set range start to current ID | |
| 428 | + | if (range_start_field) { | |
| 429 | + | range_start_field->SetValue(id); | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | // Set range end to current ID + 10 | |
| 433 | + | if (range_end_field) { | |
| 434 | + | range_end_field->SetValue(id + 10); | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | // Update range info | |
| 438 | + | UpdateRangeInfo(); | |
| 439 | + | } | |
| 440 | + | } | |
| 441 | + | } |
| @@ -0,0 +1,69 @@ | |||
| 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_ADD_ITEM_WINDOW_H_ | |
| 19 | + | #define RME_ADD_ITEM_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "main.h" | |
| 22 | + | ||
| 23 | + | #include "common_windows.h" | |
| 24 | + | ||
| 25 | + | class ContainerItemButton; | |
| 26 | + | class ContainerItemPopupMenu; | |
| 27 | + | ||
| 28 | + | class AddItemWindow : public ObjectPropertiesWindowBase { | |
| 29 | + | public: | |
| 30 | + | AddItemWindow(wxWindow* parent, TilesetCategoryType categoryType, Tileset* tilesetItem, wxPoint = wxDefaultPosition); | |
| 31 | + | ||
| 32 | + | void OnItemClicked(wxMouseEvent& event); | |
| 33 | + | void SetItemIdToItemButton(uint16_t id); | |
| 34 | + | void OnChangeItemId(wxCommandEvent& WXUNUSED(event)); | |
| 35 | + | void OnRangeToggle(wxCommandEvent& WXUNUSED(event)); | |
| 36 | + | void OnRangeFieldChange(wxSpinEvent& event); | |
| 37 | + | void OnUseCurrentItem(wxCommandEvent& event); | |
| 38 | + | void OnQuickRange(wxCommandEvent& event); | |
| 39 | + | void UpdateRangeFields(bool show); | |
| 40 | + | void UpdateRangeInfo(); | |
| 41 | + | ||
| 42 | + | void OnClickOK(wxCommandEvent&); | |
| 43 | + | void OnClickCancel(wxCommandEvent&); | |
| 44 | + | ||
| 45 | + | wxChoice* tileset_choice; | |
| 46 | + | ||
| 47 | + | protected: | |
| 48 | + | int item_id; | |
| 49 | + | ||
| 50 | + | wxSpinCtrl* item_id_field; | |
| 51 | + | wxStaticText* item_id_label; | |
| 52 | + | wxStaticText* item_name_label; | |
| 53 | + | DCButton* item_button; | |
| 54 | + | ||
| 55 | + | // Range selection controls | |
| 56 | + | wxCheckBox* range_checkbox; | |
| 57 | + | wxSpinCtrl* range_start_field; | |
| 58 | + | wxSpinCtrl* range_end_field; | |
| 59 | + | wxStaticText* range_start_label; | |
| 60 | + | wxStaticText* range_end_label; | |
| 61 | + | wxStaticText* range_info_label; | |
| 62 | + | ||
| 63 | + | TilesetCategoryType category_type; | |
| 64 | + | Tileset* tileset_item; | |
| 65 | + | ||
| 66 | + | DECLARE_EVENT_TABLE(); | |
| 67 | + | }; | |
| 68 | + | ||
| 69 | + | #endif |
| @@ -0,0 +1,160 @@ | |||
| 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/grid.h> | |
| 21 | + | ||
| 22 | + | #include "tile.h" | |
| 23 | + | #include "item.h" | |
| 24 | + | #include "complexitem.h" | |
| 25 | + | #include "town.h" | |
| 26 | + | #include "house.h" | |
| 27 | + | #include "map.h" | |
| 28 | + | #include "editor.h" | |
| 29 | + | #include "creature.h" | |
| 30 | + | #include "materials.h" | |
| 31 | + | #include "tileset.h" | |
| 32 | + | ||
| 33 | + | #include "gui.h" | |
| 34 | + | #include "application.h" | |
| 35 | + | #include "add_tileset_window.h" | |
| 36 | + | #include "container_properties_window.h" | |
| 37 | + | #include "find_item_window.h" | |
| 38 | + | ||
| 39 | + | // ============================================================================ | |
| 40 | + | // Add Tileset Window | |
| 41 | + | ||
| 42 | + | BEGIN_EVENT_TABLE(AddTilesetWindow, wxDialog) | |
| 43 | + | EVT_BUTTON(wxID_OK, AddTilesetWindow::OnClickOK) | |
| 44 | + | EVT_BUTTON(wxID_CANCEL, AddTilesetWindow::OnClickCancel) | |
| 45 | + | END_EVENT_TABLE() | |
| 46 | + | ||
| 47 | + | static constexpr int OUTFIT_COLOR_MAX = 133; | |
| 48 | + | ||
| 49 | + | AddTilesetWindow::AddTilesetWindow(wxWindow* win_parent, TilesetCategoryType categoryType, wxPoint pos) : | |
| 50 | + | ObjectPropertiesWindowBase(win_parent, "Add a Tileset", pos), | |
| 51 | + | category_type(categoryType), | |
| 52 | + | item_id_label(nullptr), | |
| 53 | + | item_name_label(nullptr), | |
| 54 | + | tileset_name_field(nullptr), | |
| 55 | + | item_id_field(nullptr), | |
| 56 | + | item_button(nullptr) { | |
| 57 | + | wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL); | |
| 58 | + | wxString description = "Add a Tileset"; | |
| 59 | + | ||
| 60 | + | wxSizer* boxsizer = newd wxStaticBoxSizer(wxVERTICAL, this, description); | |
| 61 | + | ||
| 62 | + | wxFlexGridSizer* subsizer = newd wxFlexGridSizer(2, 10, 10); | |
| 63 | + | subsizer->AddGrowableCol(1); | |
| 64 | + | ||
| 65 | + | uint16_t itemId = 0; | |
| 66 | + | std::string itemName = "None"; | |
| 67 | + | item_id_label = newd wxStaticText(this, wxID_ANY, "ID " + i2ws(itemId)); | |
| 68 | + | subsizer->Add(item_id_label); | |
| 69 | + | item_name_label = newd wxStaticText(this, wxID_ANY, "\"" + wxstr(itemName) + "\""); | |
| 70 | + | subsizer->Add(item_name_label); | |
| 71 | + | ||
| 72 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Item"), wxSizerFlags(1).CenterVertical()); | |
| 73 | + | item_button = newd DCButton(this, wxID_ANY, wxDefaultPosition, DC_BTN_TOGGLE, RENDER_SIZE_32x32, 0); | |
| 74 | + | subsizer->Add(item_button); | |
| 75 | + | ||
| 76 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Item Id of First Item")); | |
| 77 | + | item_id_field = newd wxSpinCtrl(this, wxID_ANY, i2ws(itemId), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 100, 100000); | |
| 78 | + | subsizer->Add(item_id_field, wxSizerFlags(1).Expand()); | |
| 79 | + | ||
| 80 | + | subsizer->Add(newd wxStaticText(this, wxID_ANY, "Tileset Name")); | |
| 81 | + | tileset_name_field = newd wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE); | |
| 82 | + | subsizer->Add(tileset_name_field); | |
| 83 | + | ||
| 84 | + | boxsizer->Add(subsizer, wxSizerFlags(1).Expand()); | |
| 85 | + | ||
| 86 | + | topsizer->Add(boxsizer, wxSizerFlags(0).Expand().Border(wxLEFT | wxRIGHT, 20)); | |
| 87 | + | ||
| 88 | + | wxSizer* subsizer_ = newd wxBoxSizer(wxHORIZONTAL); | |
| 89 | + | subsizer_->Add(newd wxButton(this, wxID_OK, "Add"), wxSizerFlags(1).Center().Border(wxTOP | wxBOTTOM, 10)); | |
| 90 | + | subsizer_->Add(newd wxButton(this, wxID_CANCEL, "Cancel"), wxSizerFlags(1).Center().Border(wxTOP | wxBOTTOM, 10)); | |
| 91 | + | topsizer->Add(subsizer_, wxSizerFlags(0).Center().Border(wxLEFT | wxRIGHT, 20)); | |
| 92 | + | ||
| 93 | + | SetSizerAndFit(topsizer); | |
| 94 | + | Centre(wxBOTH); | |
| 95 | + | ||
| 96 | + | item_id_field->Connect(wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler(AddTilesetWindow::OnChangeItemId), NULL, this); | |
| 97 | + | item_button->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(AddTilesetWindow::OnItemClicked), NULL, this); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | void AddTilesetWindow::OnChangeItemId(wxCommandEvent& WXUNUSED(event)) { | |
| 101 | + | uint16_t itemId = item_id_field->GetValue(); | |
| 102 | + | ItemType& it = g_items[itemId]; | |
| 103 | + | if (it.id != 0) { | |
| 104 | + | item_id_label->SetLabelText("ID " + i2ws(it.id)); | |
| 105 | + | item_name_label->SetLabelText("\"" + wxstr(it.name) + "\""); | |
| 106 | + | ||
| 107 | + | item_button->SetSprite(it.clientID); | |
| 108 | + | } else { | |
| 109 | + | item_id_field->SetValue(100); | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | void AddTilesetWindow::OnItemClicked(wxMouseEvent& WXUNUSED(event)) { | |
| 114 | + | FindItemDialog dialog(this, "Item"); | |
| 115 | + | if (dialog.ShowModal() == wxID_OK) { | |
| 116 | + | uint16_t id = dialog.getResultID(); | |
| 117 | + | SetItemIdToItemButton(id); | |
| 118 | + | } | |
| 119 | + | dialog.Destroy(); | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | void AddTilesetWindow::SetItemIdToItemButton(uint16_t id) { | |
| 123 | + | if (!item_button) { | |
| 124 | + | return; | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | if (id != 0) { | |
| 128 | + | const ItemType& it = g_items.getItemType(id); | |
| 129 | + | if (it.id != 0) { | |
| 130 | + | item_id_field->SetValue(it.id); | |
| 131 | + | item_id_label->SetLabelText("ID " + i2ws(it.id)); | |
| 132 | + | item_name_label->SetLabelText("\"" + wxstr(it.name) + "\""); | |
| 133 | + | ||
| 134 | + | item_button->SetSprite(it.clientID); | |
| 135 | + | return; | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | item_button->SetSprite(0); | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | void AddTilesetWindow::OnClickOK(wxCommandEvent& WXUNUSED(event)) { | |
| 143 | + | uint16_t itemId = item_id_field->GetValue(); | |
| 144 | + | ItemType& it = g_items[itemId]; | |
| 145 | + | if (it.id != 0) { | |
| 146 | + | std::string tilesetName = std::string(tileset_name_field->GetValue().mb_str()); | |
| 147 | + | g_materials.addToTileset(tilesetName, it.id, category_type); | |
| 148 | + | g_materials.modify(); | |
| 149 | + | g_gui.PopupDialog("Added Tileset", "'" + it.name + "' has been added to new tileset '" + tilesetName + "'", wxOK); | |
| 150 | + | ||
| 151 | + | EndModal(1); | |
| 152 | + | } else { | |
| 153 | + | g_gui.PopupDialog(this, "Error", "Item does not exist.", wxOK); | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | void AddTilesetWindow::OnClickCancel(wxCommandEvent& WXUNUSED(event)) { | |
| 158 | + | // Just close this window | |
| 159 | + | EndModal(0); | |
| 160 | + | } |
| @@ -0,0 +1,52 @@ | |||
| 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_ADD_TILESET_WINDOW_H_ | |
| 19 | + | #define RME_ADD_TILESET_WINDOW_H_ | |
| 20 | + | ||
| 21 | + | #include "main.h" | |
| 22 | + | ||
| 23 | + | #include "common_windows.h" | |
| 24 | + | ||
| 25 | + | class ContainerItemButton; | |
| 26 | + | class ContainerItemPopupMenu; | |
| 27 | + | ||
| 28 | + | class AddTilesetWindow : public ObjectPropertiesWindowBase { | |
| 29 | + | public: | |
| 30 | + | AddTilesetWindow(wxWindow* parent, TilesetCategoryType categoryType, wxPoint = wxDefaultPosition); | |
| 31 | + | ||
| 32 | + | void OnItemClicked(wxMouseEvent& event); | |
| 33 | + | void SetItemIdToItemButton(uint16_t id); | |
| 34 | + | void OnChangeItemId(wxCommandEvent& WXUNUSED(event)); | |
| 35 | + | ||
| 36 | + | void OnClickOK(wxCommandEvent&); | |
| 37 | + | void OnClickCancel(wxCommandEvent&); | |
| 38 | + | ||
| 39 | + | protected: | |
| 40 | + | wxTextCtrl* tileset_name_field; | |
| 41 | + | wxSpinCtrl* item_id_field; | |
| 42 | + | ||
| 43 | + | wxStaticText* item_id_label; | |
| 44 | + | wxStaticText* item_name_label; | |
| 45 | + | ||
| 46 | + | TilesetCategoryType category_type; | |
| 47 | + | DCButton* item_button; | |
| 48 | + | ||
| 49 | + | DECLARE_EVENT_TABLE(); | |
| 50 | + | }; | |
| 51 | + | ||
| 52 | + | #endif |