| 1 | //============================================================================ |
| 2 | // |
| 3 | // SSSS tt lll lll |
| 4 | // SS SS tt ll ll |
| 5 | // SS tttttt eeee ll ll aaaa |
| 6 | // SSSS tt ee ee ll ll aa |
| 7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
| 8 | // SS SS tt ee ll ll aa aa |
| 9 | // SSSS ttt eeeee llll llll aaaaa |
| 10 | // |
| 11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
| 12 | // and the Stella Team |
| 13 | // |
| 14 | // See the file "License.txt" for information on usage and redistribution of |
| 15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
| 16 | //============================================================================ |
| 17 | |
| 18 | #include "Dialog.hxx" |
| 19 | #include "Font.hxx" |
| 20 | #include "EventHandler.hxx" |
| 21 | #include "FrameBuffer.hxx" |
| 22 | #include "FBSurface.hxx" |
| 23 | #include "OSystem.hxx" |
| 24 | #include "Widget.hxx" |
| 25 | #include "StateManager.hxx" |
| 26 | #include "RewindManager.hxx" |
| 27 | #include "TimeLineWidget.hxx" |
| 28 | |
| 29 | #include "Console.hxx" |
| 30 | #include "TIA.hxx" |
| 31 | #include "System.hxx" |
| 32 | |
| 33 | #include "TimeMachineDialog.hxx" |
| 34 | #include "Base.hxx" |
| 35 | using Common::Base; |
| 36 | |
| 37 | const int BUTTON_W = 14, BUTTON_H = 14; |
| 38 | |
| 39 | static uInt32 RECORD[BUTTON_H] = |
| 40 | { |
| 41 | 0b00000111100000, |
| 42 | 0b00011111111000, |
| 43 | 0b00111111111100, |
| 44 | 0b01111111111110, |
| 45 | 0b01111111111110, |
| 46 | 0b11111111111111, |
| 47 | 0b11111111111111, |
| 48 | 0b11111111111111, |
| 49 | 0b11111111111111, |
| 50 | 0b01111111111110, |
| 51 | 0b01111111111110, |
| 52 | 0b00111111111100, |
| 53 | 0b00011111111000, |
| 54 | 0b00000111100000 |
| 55 | }; |
| 56 | |
| 57 | static uInt32 STOP[BUTTON_H] = |
| 58 | { |
| 59 | 0b11111111111111, |
| 60 | 0b11111111111111, |
| 61 | 0b11111111111111, |
| 62 | 0b11111111111111, |
| 63 | 0b11111111111111, |
| 64 | 0b11111111111111, |
| 65 | 0b11111111111111, |
| 66 | 0b11111111111111, |
| 67 | 0b11111111111111, |
| 68 | 0b11111111111111, |
| 69 | 0b11111111111111, |
| 70 | 0b11111111111111, |
| 71 | 0b11111111111111, |
| 72 | 0b11111111111111 |
| 73 | }; |
| 74 | |
| 75 | static uInt32 PLAY[BUTTON_H] = |
| 76 | { |
| 77 | 0b11000000000000, |
| 78 | 0b11110000000000, |
| 79 | 0b11111100000000, |
| 80 | 0b11111111000000, |
| 81 | 0b11111111110000, |
| 82 | 0b11111111111100, |
| 83 | 0b11111111111111, |
| 84 | 0b11111111111111, |
| 85 | 0b11111111111100, |
| 86 | 0b11111111110000, |
| 87 | 0b11111111000000, |
| 88 | 0b11111100000000, |
| 89 | 0b11110000000000, |
| 90 | 0b11000000000000 |
| 91 | }; |
| 92 | static uInt32 REWIND_ALL[BUTTON_H] = |
| 93 | { |
| 94 | 0, |
| 95 | 0b11000011000011, |
| 96 | 0b11000111000111, |
| 97 | 0b11001111001111, |
| 98 | 0b11011111011111, |
| 99 | 0b11111111111111, |
| 100 | 0b11111111111111, |
| 101 | 0b11111111111111, |
| 102 | 0b11111111111111, |
| 103 | 0b11011111011111, |
| 104 | 0b11001111001111, |
| 105 | 0b11000111000111, |
| 106 | 0b11000011000011, |
| 107 | 0 |
| 108 | }; |
| 109 | static uInt32 REWIND_1[BUTTON_H] = |
| 110 | { |
| 111 | 0, |
| 112 | 0b00000110001110, |
| 113 | 0b00001110001110, |
| 114 | 0b00011110001110, |
| 115 | 0b00111110001110, |
| 116 | 0b01111110001110, |
| 117 | 0b11111110001110, |
| 118 | 0b11111110001110, |
| 119 | 0b01111110001110, |
| 120 | 0b00111110001110, |
| 121 | 0b00011110001110, |
| 122 | 0b00001110001110, |
| 123 | 0b00000110001110, |
| 124 | 0 |
| 125 | }; |
| 126 | static uInt32 UNWIND_1[BUTTON_H] = |
| 127 | { |
| 128 | 0, |
| 129 | 0b01110001100000, |
| 130 | 0b01110001110000, |
| 131 | 0b01110001111000, |
| 132 | 0b01110001111100, |
| 133 | 0b01110001111110, |
| 134 | 0b01110001111111, |
| 135 | 0b01110001111111, |
| 136 | 0b01110001111110, |
| 137 | 0b01110001111100, |
| 138 | 0b01110001111000, |
| 139 | 0b01110001110000, |
| 140 | 0b01110001100000, |
| 141 | 0 |
| 142 | }; |
| 143 | static uInt32 UNWIND_ALL[BUTTON_H] = |
| 144 | { |
| 145 | 0, |
| 146 | 0b11000011000011, |
| 147 | 0b11100011100011, |
| 148 | 0b11110011110011, |
| 149 | 0b11111011111011, |
| 150 | 0b11111111111111, |
| 151 | 0b11111111111111, |
| 152 | 0b11111111111111, |
| 153 | 0b11111111111111, |
| 154 | 0b11111011111011, |
| 155 | 0b11110011110011, |
| 156 | 0b11100011100011, |
| 157 | 0b11000011000011, |
| 158 | 0 |
| 159 | }; |
| 160 | static uInt32 SAVE_ALL[BUTTON_H] = |
| 161 | { |
| 162 | 0b00000111100000, |
| 163 | 0b00000111100000, |
| 164 | 0b00000111100000, |
| 165 | 0b00000111100000, |
| 166 | 0b11111111111111, |
| 167 | 0b01111111111110, |
| 168 | 0b00111111111100, |
| 169 | 0b00011111111000, |
| 170 | 0b00001111110000, |
| 171 | 0b00000111100000, |
| 172 | 0b00000011000000, |
| 173 | 0b00000000000000, |
| 174 | 0b11111111111111, |
| 175 | 0b11111111111111, |
| 176 | }; |
| 177 | static uInt32 LOAD_ALL[BUTTON_H] = |
| 178 | { |
| 179 | 0b00000011000000, |
| 180 | 0b00000111100000, |
| 181 | 0b00001111110000, |
| 182 | 0b00011111111000, |
| 183 | 0b00111111111100, |
| 184 | 0b01111111111110, |
| 185 | 0b11111111111111, |
| 186 | 0b00000111100000, |
| 187 | 0b00000111100000, |
| 188 | 0b00000111100000, |
| 189 | 0b00000111100000, |
| 190 | 0b00000000000000, |
| 191 | 0b11111111111111, |
| 192 | 0b11111111111111, |
| 193 | }; |
| 194 | |
| 195 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 196 | TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent, |
| 197 | int width) |
| 198 | : Dialog(osystem, parent), |
| 199 | _enterWinds(0) |
| 200 | { |
| 201 | const GUI::Font& font = instance().frameBuffer().font(); |
| 202 | const int H_BORDER = 6, BUTTON_GAP = 4, V_BORDER = 4; |
| 203 | const int buttonWidth = BUTTON_W + 10, |
| 204 | buttonHeight = BUTTON_H + 10, |
| 205 | rowHeight = font.getLineHeight(); |
| 206 | |
| 207 | int xpos, ypos; |
| 208 | |
| 209 | // Set real dimensions |
| 210 | _w = width; // Parent determines our width (based on window size) |
| 211 | _h = V_BORDER * 2 + rowHeight + buttonHeight + 2; |
| 212 | |
| 213 | this->clearFlags(Widget::FLAG_CLEARBG); // does only work combined with blending (0..100)! |
| 214 | this->clearFlags(Widget::FLAG_BORDER); |
| 215 | |
| 216 | xpos = H_BORDER; |
| 217 | ypos = V_BORDER; |
| 218 | |
| 219 | // Add index info |
| 220 | myCurrentIdxWidget = new StaticTextWidget(this, font, xpos, ypos, "1000" , TextAlign::Left, kBGColor); |
| 221 | myCurrentIdxWidget->setTextColor(kColorInfo); |
| 222 | myLastIdxWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("1000" ), ypos, |
| 223 | "1000" , TextAlign::Right, kBGColor); |
| 224 | myLastIdxWidget->setTextColor(kColorInfo); |
| 225 | |
| 226 | // Add timeline |
| 227 | const uInt32 tl_h = myCurrentIdxWidget->getHeight() / 2 + 6, |
| 228 | tl_x = xpos + myCurrentIdxWidget->getWidth() + 8, |
| 229 | tl_y = ypos + (myCurrentIdxWidget->getHeight() - tl_h) / 2 - 1, |
| 230 | tl_w = myLastIdxWidget->getAbsX() - tl_x - 8; |
| 231 | myTimeline = new TimeLineWidget(this, font, tl_x, tl_y, tl_w, tl_h, "" , 0, kTimeline); |
| 232 | myTimeline->setMinValue(0); |
| 233 | ypos += rowHeight; |
| 234 | |
| 235 | // Add time info |
| 236 | myCurrentTimeWidget = new StaticTextWidget(this, font, xpos, ypos + 3, "00:00.00" , TextAlign::Left, kBGColor); |
| 237 | myCurrentTimeWidget->setTextColor(kColorInfo); |
| 238 | myLastTimeWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("00:00.00" ), ypos + 3, |
| 239 | "00:00.00" , TextAlign::Right, kBGColor); |
| 240 | myLastTimeWidget->setTextColor(kColorInfo); |
| 241 | xpos = myCurrentTimeWidget->getRight() + BUTTON_GAP * 4; |
| 242 | |
| 243 | // Add buttons |
| 244 | myToggleWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, STOP, |
| 245 | BUTTON_W, BUTTON_H, kToggle); |
| 246 | xpos += buttonWidth + BUTTON_GAP; |
| 247 | |
| 248 | myPlayWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, PLAY, |
| 249 | BUTTON_W, BUTTON_H, kPlay); |
| 250 | xpos += buttonWidth + BUTTON_GAP * 4; |
| 251 | |
| 252 | myRewindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, REWIND_ALL, |
| 253 | BUTTON_W, BUTTON_H, kRewindAll); |
| 254 | xpos += buttonWidth + BUTTON_GAP; |
| 255 | |
| 256 | myRewind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, REWIND_1, |
| 257 | BUTTON_W, BUTTON_H, kRewind1, true); |
| 258 | xpos += buttonWidth + BUTTON_GAP; |
| 259 | |
| 260 | myUnwind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, UNWIND_1, |
| 261 | BUTTON_W, BUTTON_H, kUnwind1, true); |
| 262 | xpos += buttonWidth + BUTTON_GAP; |
| 263 | |
| 264 | myUnwindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, UNWIND_ALL, |
| 265 | BUTTON_W, BUTTON_H, kUnwindAll); |
| 266 | xpos = myUnwindAllWidget->getRight() + BUTTON_GAP * 4; |
| 267 | |
| 268 | mySaveAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, SAVE_ALL, |
| 269 | BUTTON_W, BUTTON_H, kSaveAll); |
| 270 | xpos = mySaveAllWidget->getRight() + BUTTON_GAP; |
| 271 | |
| 272 | myLoadAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, LOAD_ALL, |
| 273 | BUTTON_W, BUTTON_H, kLoadAll); |
| 274 | xpos = myLoadAllWidget->getRight() + BUTTON_GAP * 4; |
| 275 | |
| 276 | // Add message |
| 277 | myMessageWidget = new StaticTextWidget(this, font, xpos, ypos + 3, " " , |
| 278 | TextAlign::Left, kBGColor); |
| 279 | myMessageWidget->setTextColor(kColorInfo); |
| 280 | } |
| 281 | |
| 282 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 283 | void TimeMachineDialog::center() |
| 284 | { |
| 285 | // Place on the bottom of the screen, centered horizontally |
| 286 | const Common::Size& screen = instance().frameBuffer().screenSize(); |
| 287 | const Common::Rect& dst = surface().dstRect(); |
| 288 | surface().setDstPos((screen.w - dst.w()) >> 1, screen.h - dst.h() - 10); |
| 289 | } |
| 290 | |
| 291 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 292 | void TimeMachineDialog::loadConfig() |
| 293 | { |
| 294 | // Enable blending (only once is necessary) |
| 295 | if(!surface().attributes().blending) |
| 296 | { |
| 297 | surface().attributes().blending = true; |
| 298 | surface().attributes().blendalpha = 92; |
| 299 | surface().applyAttributes(); |
| 300 | } |
| 301 | |
| 302 | initBar(); |
| 303 | } |
| 304 | |
| 305 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 306 | void TimeMachineDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated) |
| 307 | { |
| 308 | // The following 'Alt' shortcuts duplicate the shortcuts in EventHandler |
| 309 | // It is best to keep them the same, so changes in EventHandler mean we |
| 310 | // need to update the logic here too |
| 311 | if(StellaModTest::isAlt(mod)) |
| 312 | { |
| 313 | switch(key) |
| 314 | { |
| 315 | case KBDK_LEFT: // Alt-left(-shift) rewinds 1(10) states |
| 316 | handleCommand(nullptr, StellaModTest::isShift(mod) ? kRewind10 : kRewind1, 0, 0); |
| 317 | break; |
| 318 | |
| 319 | case KBDK_RIGHT: // Alt-right(-shift) unwinds 1(10) states |
| 320 | handleCommand(nullptr, StellaModTest::isShift(mod) ? kUnwind10 : kUnwind1, 0, 0); |
| 321 | break; |
| 322 | |
| 323 | case KBDK_DOWN: // Alt-down rewinds to start of list |
| 324 | handleCommand(nullptr, kRewindAll, 0, 0); |
| 325 | break; |
| 326 | |
| 327 | case KBDK_UP: // Alt-up rewinds to end of list |
| 328 | handleCommand(nullptr, kUnwindAll, 0, 0); |
| 329 | break; |
| 330 | |
| 331 | default: |
| 332 | Dialog::handleKeyDown(key, mod); |
| 333 | } |
| 334 | } |
| 335 | else if(key == KBDK_SPACE || key == KBDK_ESCAPE) |
| 336 | handleCommand(nullptr, kPlay, 0, 0); |
| 337 | else |
| 338 | Dialog::handleKeyDown(key, mod); |
| 339 | } |
| 340 | |
| 341 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 342 | void TimeMachineDialog::handleCommand(CommandSender* sender, int cmd, |
| 343 | int data, int id) |
| 344 | { |
| 345 | switch(cmd) |
| 346 | { |
| 347 | case kTimeline: |
| 348 | { |
| 349 | Int32 winds = myTimeline->getValue() - |
| 350 | instance().state().rewindManager().getCurrentIdx() + 1; |
| 351 | handleWinds(winds); |
| 352 | break; |
| 353 | } |
| 354 | |
| 355 | case kToggle: |
| 356 | instance().state().toggleTimeMachine(); |
| 357 | handleToggle(); |
| 358 | break; |
| 359 | |
| 360 | case kPlay: |
| 361 | instance().eventHandler().leaveMenuMode(); |
| 362 | break; |
| 363 | |
| 364 | case kRewind1: |
| 365 | handleWinds(-1); |
| 366 | break; |
| 367 | |
| 368 | case kRewind10: |
| 369 | handleWinds(-10); |
| 370 | break; |
| 371 | |
| 372 | case kRewindAll: |
| 373 | handleWinds(-1000); |
| 374 | break; |
| 375 | |
| 376 | case kUnwind1: |
| 377 | handleWinds(1); |
| 378 | break; |
| 379 | |
| 380 | case kUnwind10: |
| 381 | handleWinds(10); |
| 382 | break; |
| 383 | |
| 384 | case kUnwindAll: |
| 385 | handleWinds(1000); |
| 386 | break; |
| 387 | |
| 388 | case kSaveAll: |
| 389 | instance().frameBuffer().showMessage(instance().state().rewindManager().saveAllStates()); |
| 390 | break; |
| 391 | |
| 392 | case kLoadAll: |
| 393 | instance().frameBuffer().showMessage(instance().state().rewindManager().loadAllStates()); |
| 394 | initBar(); |
| 395 | break; |
| 396 | |
| 397 | default: |
| 398 | Dialog::handleCommand(sender, cmd, data, 0); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 403 | void TimeMachineDialog::initBar() |
| 404 | { |
| 405 | RewindManager& r = instance().state().rewindManager(); |
| 406 | IntArray cycles = r.cyclesList(); |
| 407 | |
| 408 | // Set range and intervals for timeline |
| 409 | uInt32 maxValue = cycles.size() > 1 ? uInt32(cycles.size() - 1) : 0; |
| 410 | myTimeline->setMaxValue(maxValue); |
| 411 | myTimeline->setStepValues(cycles); |
| 412 | |
| 413 | myMessageWidget->setLabel("" ); |
| 414 | handleWinds(_enterWinds); |
| 415 | _enterWinds = 0; |
| 416 | |
| 417 | handleToggle(); |
| 418 | } |
| 419 | |
| 420 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 421 | string TimeMachineDialog::getTimeString(uInt64 cycles) |
| 422 | { |
| 423 | const Int32 scanlines = std::max(instance().console().tia().scanlinesLastFrame(), 240u); |
| 424 | const bool isNTSC = scanlines <= 287; |
| 425 | const Int32 NTSC_FREQ = 1193182; // ~76*262*60 |
| 426 | const Int32 PAL_FREQ = 1182298; // ~76*312*50 |
| 427 | const Int32 freq = isNTSC ? NTSC_FREQ : PAL_FREQ; // = cycles/second |
| 428 | |
| 429 | uInt32 minutes = uInt32(cycles / (freq * 60)); |
| 430 | cycles -= minutes * (freq * 60); |
| 431 | uInt32 seconds = uInt32(cycles / freq); |
| 432 | cycles -= seconds * freq; |
| 433 | uInt32 frames = uInt32(cycles / (scanlines * 76)); |
| 434 | |
| 435 | stringstream time; |
| 436 | time << Common::Base::toString(minutes, Common::Base::F_10_02) << ":" ; |
| 437 | time << Common::Base::toString(seconds, Common::Base::F_10_02) << "." ; |
| 438 | time << Common::Base::toString(frames, Common::Base::F_10_02); |
| 439 | |
| 440 | return time.str(); |
| 441 | } |
| 442 | |
| 443 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 444 | void TimeMachineDialog::handleWinds(Int32 numWinds) |
| 445 | { |
| 446 | RewindManager& r = instance().state().rewindManager(); |
| 447 | |
| 448 | if(numWinds) |
| 449 | { |
| 450 | uInt64 startCycles = r.getCurrentCycles(); |
| 451 | if(numWinds < 0) r.rewindStates(-numWinds); |
| 452 | else if(numWinds > 0) r.unwindStates(numWinds); |
| 453 | |
| 454 | uInt64 elapsed = instance().console().tia().cycles() - startCycles; |
| 455 | if(elapsed > 0) |
| 456 | { |
| 457 | string message = r.getUnitString(elapsed); |
| 458 | |
| 459 | // TODO: add message text from addState() |
| 460 | myMessageWidget->setLabel((numWinds < 0 ? "(-" : "(+" ) + message + ")" ); |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | // Update time |
| 465 | myCurrentTimeWidget->setLabel(getTimeString(r.getCurrentCycles() - r.getFirstCycles())); |
| 466 | myLastTimeWidget->setLabel(getTimeString(r.getLastCycles() - r.getFirstCycles())); |
| 467 | myTimeline->setValue(r.getCurrentIdx()-1); |
| 468 | // Update index |
| 469 | myCurrentIdxWidget->setValue(r.getCurrentIdx()); |
| 470 | myLastIdxWidget->setValue(r.getLastIdx()); |
| 471 | // Enable/disable buttons |
| 472 | myRewindAllWidget->setEnabled(!r.atFirst()); |
| 473 | myRewind1Widget->setEnabled(!r.atFirst()); |
| 474 | myUnwindAllWidget->setEnabled(!r.atLast()); |
| 475 | myUnwind1Widget->setEnabled(!r.atLast()); |
| 476 | mySaveAllWidget->setEnabled(r.getLastIdx() != 0); |
| 477 | } |
| 478 | |
| 479 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 480 | void TimeMachineDialog::handleToggle() |
| 481 | { |
| 482 | myToggleWidget->setBitmap(instance().state().mode() == StateManager::Mode::Off ? RECORD : STOP, |
| 483 | BUTTON_W, BUTTON_H); |
| 484 | } |
| 485 | |