1 | // SuperTux - Console |
2 | // Copyright (C) 2006 Christoph Sommer <christoph.sommer@2006.expires.deltadevelopment.de> |
3 | // |
4 | // This program 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 | // This program 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 | #include "supertux/console.hpp" |
18 | |
19 | #include "math/sizef.hpp" |
20 | #include "physfs/ifile_stream.hpp" |
21 | #include "squirrel/squirrel_virtual_machine.hpp" |
22 | #include "squirrel/squirrel_util.hpp" |
23 | #include "supertux/gameconfig.hpp" |
24 | #include "supertux/globals.hpp" |
25 | #include "supertux/resources.hpp" |
26 | #include "util/log.hpp" |
27 | #include "video/drawing_context.hpp" |
28 | #include "video/surface.hpp" |
29 | |
30 | /// speed (pixels/s) the console closes |
31 | static const float FADE_SPEED = 1; |
32 | |
33 | ConsoleBuffer::ConsoleBuffer() : |
34 | m_lines(), |
35 | m_console(nullptr) |
36 | { |
37 | } |
38 | |
39 | void |
40 | ConsoleBuffer::set_console(Console* console) |
41 | { |
42 | assert((console && !m_console) || |
43 | (m_console && !console)); |
44 | |
45 | m_console = console; |
46 | } |
47 | |
48 | void |
49 | ConsoleBuffer::addLines(const std::string& s) |
50 | { |
51 | std::istringstream iss(s); |
52 | std::string line; |
53 | while (std::getline(iss, line, '\n')) |
54 | { |
55 | addLine(line); |
56 | } |
57 | } |
58 | |
59 | void |
60 | ConsoleBuffer::addLine(const std::string& s_) |
61 | { |
62 | std::string s = s_; |
63 | |
64 | // output line to stderr |
65 | std::cerr << s << std::endl; |
66 | |
67 | // wrap long lines |
68 | std::string overflow; |
69 | int line_count = 0; |
70 | do { |
71 | m_lines.push_front(Font::wrap_to_chars(s, 99, &overflow)); |
72 | line_count += 1; |
73 | s = overflow; |
74 | } while (s.length() > 0); |
75 | |
76 | // trim scrollback buffer |
77 | while (m_lines.size() >= 1000) |
78 | { |
79 | m_lines.pop_back(); |
80 | } |
81 | |
82 | if (m_console) |
83 | { |
84 | m_console->on_buffer_change(line_count); |
85 | } |
86 | } |
87 | |
88 | void |
89 | ConsoleBuffer::flush(ConsoleStreamBuffer& buffer) |
90 | { |
91 | if (&buffer == &s_outputBuffer) |
92 | { |
93 | std::string s = s_outputBuffer.str(); |
94 | if ((s.length() > 0) && ((s[s.length()-1] == '\n') || (s[s.length()-1] == '\r'))) |
95 | { |
96 | while ((s[s.length()-1] == '\n') || (s[s.length()-1] == '\r')) |
97 | { |
98 | s.erase(s.length()-1); |
99 | } |
100 | addLines(s); |
101 | s_outputBuffer.str(std::string()); |
102 | } |
103 | } |
104 | } |
105 | |
106 | Console::Console(ConsoleBuffer& buffer) : |
107 | m_buffer(buffer), |
108 | m_inputBuffer(), |
109 | m_inputBufferPosition(0), |
110 | m_history(), |
111 | m_history_position(m_history.end()), |
112 | m_background(Surface::from_file("images/engine/console.png" )), |
113 | m_background2(Surface::from_file("images/engine/console2.png" )), |
114 | m_vm(nullptr), |
115 | m_vm_object(), |
116 | m_backgroundOffset(0), |
117 | m_height(0), |
118 | m_alpha(1.0), |
119 | m_offset(0), |
120 | m_focused(false), |
121 | m_font(Resources::console_font), |
122 | m_stayOpen(0) |
123 | { |
124 | m_buffer.set_console(this); |
125 | } |
126 | |
127 | Console::~Console() |
128 | { |
129 | if (m_vm != nullptr && SquirrelVirtualMachine::current() != nullptr) |
130 | { |
131 | sq_release(SquirrelVirtualMachine::current()->get_vm().get_vm(), &m_vm_object); |
132 | } |
133 | m_buffer.set_console(nullptr); |
134 | } |
135 | |
136 | void |
137 | Console::on_buffer_change(int line_count) |
138 | { |
139 | // increase console height if necessary |
140 | if (m_stayOpen > 0 && m_height < 64) |
141 | { |
142 | if (m_height < 4) |
143 | { |
144 | m_height = 4; |
145 | } |
146 | m_height += m_font->get_height() * static_cast<float>(line_count); |
147 | } |
148 | |
149 | // reset console to full opacity |
150 | m_alpha = 1.0; |
151 | } |
152 | |
153 | void |
154 | Console::ready_vm() |
155 | { |
156 | if (m_vm == nullptr) { |
157 | m_vm = SquirrelVirtualMachine::current()->get_vm().get_vm(); |
158 | HSQUIRRELVM new_vm = sq_newthread(m_vm, 16); |
159 | if (new_vm == nullptr) |
160 | throw SquirrelError(m_vm, "Couldn't create new VM thread for console" ); |
161 | |
162 | // store reference to thread |
163 | sq_resetobject(&m_vm_object); |
164 | if (SQ_FAILED(sq_getstackobj(m_vm, -1, &m_vm_object))) |
165 | throw SquirrelError(m_vm, "Couldn't get vm object for console" ); |
166 | sq_addref(m_vm, &m_vm_object); |
167 | sq_pop(m_vm, 1); |
168 | |
169 | // create new roottable for thread |
170 | sq_newtable(new_vm); |
171 | sq_pushroottable(new_vm); |
172 | if (SQ_FAILED(sq_setdelegate(new_vm, -2))) |
173 | throw SquirrelError(new_vm, "Couldn't set console_table delegate" ); |
174 | |
175 | sq_setroottable(new_vm); |
176 | |
177 | m_vm = new_vm; |
178 | |
179 | try { |
180 | std::string filename = "scripts/console.nut" ; |
181 | IFileStream stream(filename); |
182 | compile_and_run(m_vm, stream, filename); |
183 | } catch(std::exception& e) { |
184 | log_warning << "Couldn't load console.nut: " << e.what() << std::endl; |
185 | } |
186 | } |
187 | } |
188 | |
189 | void |
190 | Console::execute_script(const std::string& command) |
191 | { |
192 | ready_vm(); |
193 | |
194 | SQInteger oldtop = sq_gettop(m_vm); |
195 | try { |
196 | if (SQ_FAILED(sq_compilebuffer(m_vm, command.c_str(), command.length(), |
197 | "" , SQTrue))) |
198 | throw SquirrelError(m_vm, "Couldn't compile command" ); |
199 | |
200 | sq_pushroottable(m_vm); |
201 | if (SQ_FAILED(sq_call(m_vm, 1, SQTrue, SQTrue))) |
202 | throw SquirrelError(m_vm, "Problem while executing command" ); |
203 | |
204 | if (sq_gettype(m_vm, -1) != OT_NULL) |
205 | m_buffer.addLines(squirrel2string(m_vm, -1)); |
206 | } catch(std::exception& e) { |
207 | m_buffer.addLines(e.what()); |
208 | } |
209 | SQInteger newtop = sq_gettop(m_vm); |
210 | if (newtop < oldtop) { |
211 | log_fatal << "Script destroyed squirrel stack..." << std::endl; |
212 | } else { |
213 | sq_settop(m_vm, oldtop); |
214 | } |
215 | } |
216 | |
217 | void |
218 | Console::input(char c) |
219 | { |
220 | m_inputBuffer.insert(m_inputBufferPosition, 1, c); |
221 | m_inputBufferPosition++; |
222 | } |
223 | |
224 | void |
225 | Console::backspace() |
226 | { |
227 | if ((m_inputBufferPosition > 0) && (m_inputBuffer.length() > 0)) { |
228 | m_inputBuffer.erase(m_inputBufferPosition-1, 1); |
229 | m_inputBufferPosition--; |
230 | } |
231 | } |
232 | |
233 | void |
234 | Console::eraseChar() |
235 | { |
236 | if (m_inputBufferPosition < static_cast<int>(m_inputBuffer.length())) { |
237 | m_inputBuffer.erase(m_inputBufferPosition, 1); |
238 | } |
239 | } |
240 | |
241 | void |
242 | Console::enter() |
243 | { |
244 | m_buffer.addLines("> " + m_inputBuffer); |
245 | parse(m_inputBuffer); |
246 | m_inputBuffer = "" ; |
247 | m_inputBufferPosition = 0; |
248 | } |
249 | |
250 | void |
251 | Console::scroll(int numLines) |
252 | { |
253 | m_offset += numLines; |
254 | if (m_offset > 0) m_offset = 0; |
255 | } |
256 | |
257 | void |
258 | Console::show_history(int offset_) |
259 | { |
260 | while ((offset_ > 0) && (m_history_position != m_history.end())) { |
261 | ++m_history_position; |
262 | offset_--; |
263 | } |
264 | while ((offset_ < 0) && (m_history_position != m_history.begin())) { |
265 | --m_history_position; |
266 | offset_++; |
267 | } |
268 | if (m_history_position == m_history.end()) { |
269 | m_inputBuffer = "" ; |
270 | m_inputBufferPosition = 0; |
271 | } else { |
272 | m_inputBuffer = *m_history_position; |
273 | m_inputBufferPosition = static_cast<int>(m_inputBuffer.length()); |
274 | } |
275 | } |
276 | |
277 | void |
278 | Console::move_cursor(int offset_) |
279 | { |
280 | if (offset_ == -65535) m_inputBufferPosition = 0; |
281 | if (offset_ == +65535) m_inputBufferPosition = static_cast<int>(m_inputBuffer.length()); |
282 | m_inputBufferPosition+=offset_; |
283 | if (m_inputBufferPosition < 0) m_inputBufferPosition = 0; |
284 | if (m_inputBufferPosition > static_cast<int>(m_inputBuffer.length())) m_inputBufferPosition = static_cast<int>(m_inputBuffer.length()); |
285 | } |
286 | |
287 | // Helper functions for Console::autocomplete |
288 | // TODO: Fix rough documentation |
289 | namespace { |
290 | |
291 | void sq_insert_commands(std::list<std::string>& cmds, HSQUIRRELVM vm, const std::string& table_prefix, const std::string& search_prefix); |
292 | |
293 | /** |
294 | * Acts upon key,value on top of stack: |
295 | * Appends key (plus type-dependent suffix) to cmds if table_prefix+key starts with search_prefix; |
296 | * Calls sq_insert_commands if search_prefix starts with table_prefix+key (and value is a table/class/instance); |
297 | */ |
298 | void |
299 | sq_insert_command(std::list<std::string>& cmds, HSQUIRRELVM vm, const std::string& table_prefix, const std::string& search_prefix) |
300 | { |
301 | const SQChar* key_chars; |
302 | if (SQ_FAILED(sq_getstring(vm, -2, &key_chars))) return; |
303 | std::string key_string = table_prefix + key_chars; |
304 | |
305 | switch (sq_gettype(vm, -1)) { |
306 | case OT_INSTANCE: |
307 | key_string+="." ; |
308 | if (search_prefix.substr(0, key_string.length()) == key_string) { |
309 | sq_getclass(vm, -1); |
310 | sq_insert_commands(cmds, vm, key_string, search_prefix); |
311 | sq_pop(vm, 1); |
312 | } |
313 | break; |
314 | case OT_TABLE: |
315 | case OT_CLASS: |
316 | key_string+="." ; |
317 | if (search_prefix.substr(0, key_string.length()) == key_string) { |
318 | sq_insert_commands(cmds, vm, key_string, search_prefix); |
319 | } |
320 | break; |
321 | case OT_CLOSURE: |
322 | case OT_NATIVECLOSURE: |
323 | key_string+="()" ; |
324 | break; |
325 | default: |
326 | break; |
327 | } |
328 | |
329 | if (key_string.substr(0, search_prefix.length()) == search_prefix) { |
330 | cmds.push_back(key_string); |
331 | } |
332 | |
333 | } |
334 | |
335 | /** |
336 | * calls sq_insert_command for all entries of table/class on top of stack |
337 | */ |
338 | void |
339 | sq_insert_commands(std::list<std::string>& cmds, HSQUIRRELVM vm, const std::string& table_prefix, const std::string& search_prefix) |
340 | { |
341 | sq_pushnull(vm); // push iterator |
342 | while (SQ_SUCCEEDED(sq_next(vm,-2))) { |
343 | sq_insert_command(cmds, vm, table_prefix, search_prefix); |
344 | sq_pop(vm, 2); // pop key, val |
345 | } |
346 | sq_pop(vm, 1); // pop iterator |
347 | } |
348 | |
349 | } |
350 | // End of Console::autocomplete helper functions |
351 | |
352 | void |
353 | Console::autocomplete() |
354 | { |
355 | //int autocompleteFrom = m_inputBuffer.find_last_of(" ();+", m_inputBufferPosition); |
356 | int autocompleteFrom = static_cast<int>(m_inputBuffer.find_last_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_->." , m_inputBufferPosition)); |
357 | if (autocompleteFrom != static_cast<int>(std::string::npos)) { |
358 | autocompleteFrom += 1; |
359 | } else { |
360 | autocompleteFrom = 0; |
361 | } |
362 | std::string prefix = m_inputBuffer.substr(autocompleteFrom, m_inputBufferPosition - autocompleteFrom); |
363 | m_buffer.addLines("> " + prefix); |
364 | |
365 | std::list<std::string> cmds; |
366 | |
367 | ready_vm(); |
368 | |
369 | // append all keys of the current root table to list |
370 | sq_pushroottable(m_vm); // push root table |
371 | while (true) { |
372 | // check all keys (and their children) for matches |
373 | sq_insert_commands(cmds, m_vm, "" , prefix); |
374 | |
375 | // cycle through parent(delegate) table |
376 | SQInteger oldtop = sq_gettop(m_vm); |
377 | if (SQ_FAILED(sq_getdelegate(m_vm, -1)) || oldtop == sq_gettop(m_vm)) { |
378 | break; |
379 | } |
380 | sq_remove(m_vm, -2); // remove old table |
381 | } |
382 | sq_pop(m_vm, 1); // remove table |
383 | |
384 | // depending on number of hits, show matches or autocomplete |
385 | if (cmds.empty()) |
386 | { |
387 | m_buffer.addLines("No known command starts with \"" + prefix + "\"" ); |
388 | } |
389 | |
390 | if (cmds.size() == 1) |
391 | { |
392 | // one match: just replace input buffer with full command |
393 | std::string replaceWith = cmds.front(); |
394 | m_inputBuffer.replace(autocompleteFrom, prefix.length(), replaceWith); |
395 | m_inputBufferPosition += static_cast<int>(replaceWith.length() - prefix.length()); |
396 | } |
397 | |
398 | if (cmds.size() > 1) |
399 | { |
400 | // multiple matches: show all matches and set input buffer to longest common prefix |
401 | std::string commonPrefix = cmds.front(); |
402 | while (cmds.begin() != cmds.end()) { |
403 | std::string cmd = cmds.front(); |
404 | cmds.pop_front(); |
405 | m_buffer.addLines(cmd); |
406 | for (int n = static_cast<int>(commonPrefix.length()); n >= 1; n--) { |
407 | if (cmd.compare(0, n, commonPrefix) != 0) commonPrefix.resize(n-1); else break; |
408 | } |
409 | } |
410 | std::string replaceWith = commonPrefix; |
411 | m_inputBuffer.replace(autocompleteFrom, prefix.length(), replaceWith); |
412 | m_inputBufferPosition += static_cast<int>(replaceWith.length() - prefix.length()); |
413 | } |
414 | } |
415 | |
416 | void |
417 | Console::parse(const std::string& s) |
418 | { |
419 | // make sure we actually have something to parse |
420 | if (s.length() == 0) return; |
421 | |
422 | // add line to history |
423 | m_history.push_back(s); |
424 | m_history_position = m_history.end(); |
425 | |
426 | // split line into list of args |
427 | std::vector<std::string> args; |
428 | size_t end = 0; |
429 | while (1) { |
430 | size_t start = s.find_first_not_of(" ," , end); |
431 | end = s.find_first_of(" ," , start); |
432 | if (start == s.npos) break; |
433 | args.push_back(s.substr(start, end-start)); |
434 | } |
435 | |
436 | // command is args[0] |
437 | if (args.size() == 0) return; |
438 | std::string command = args.front(); |
439 | args.erase(args.begin()); |
440 | |
441 | // ignore if it's an internal command |
442 | if (consoleCommand(command,args)) return; |
443 | |
444 | try { |
445 | execute_script(s); |
446 | } catch(std::exception& e) { |
447 | m_buffer.addLines(e.what()); |
448 | } |
449 | } |
450 | |
451 | bool |
452 | Console::consoleCommand(const std::string& /*command*/, const std::vector<std::string>& /*arguments*/) |
453 | { |
454 | return false; |
455 | } |
456 | |
457 | bool |
458 | Console::hasFocus() const |
459 | { |
460 | return m_focused; |
461 | } |
462 | |
463 | void |
464 | Console::show() |
465 | { |
466 | if (!g_config->developer_mode) |
467 | return; |
468 | |
469 | m_focused = true; |
470 | m_height = 256; |
471 | m_alpha = 1.0; |
472 | } |
473 | |
474 | void |
475 | Console::open() |
476 | { |
477 | if (m_stayOpen < 2) |
478 | m_stayOpen += 1.5f; |
479 | } |
480 | |
481 | void |
482 | Console::hide() |
483 | { |
484 | m_focused = false; |
485 | m_height = 0; |
486 | m_stayOpen = 0; |
487 | |
488 | // clear input buffer |
489 | } |
490 | |
491 | void |
492 | Console::toggle() |
493 | { |
494 | if (Console::hasFocus()) { |
495 | Console::hide(); |
496 | } |
497 | else { |
498 | Console::show(); |
499 | } |
500 | } |
501 | |
502 | void |
503 | Console::update(float dt_sec) |
504 | { |
505 | if (m_stayOpen > 0) { |
506 | m_stayOpen -= dt_sec; |
507 | if (m_stayOpen < 0) |
508 | m_stayOpen = 0; |
509 | } else if (!m_focused && m_height > 0) { |
510 | m_alpha -= dt_sec * FADE_SPEED; |
511 | if (m_alpha < 0) { |
512 | m_alpha = 0; |
513 | m_height = 0; |
514 | } |
515 | } |
516 | |
517 | m_backgroundOffset += static_cast<int>(600.0f * dt_sec); |
518 | if (m_backgroundOffset > static_cast<int>(m_background->get_width())) m_backgroundOffset -= static_cast<int>(m_background->get_width()); |
519 | } |
520 | |
521 | void |
522 | Console::draw(DrawingContext& context) const |
523 | { |
524 | if (m_height == 0) |
525 | return; |
526 | |
527 | const int layer = LAYER_GUI + 1; |
528 | const int context_center_x = context.get_width() / 2; |
529 | const int background_center_x = m_background->get_width() / 2; |
530 | |
531 | context.push_transform(); |
532 | context.set_alpha(m_alpha); |
533 | context.color().draw_surface(m_background2, |
534 | Vector(static_cast<float>(context_center_x - background_center_x - m_background->get_width() + m_backgroundOffset), |
535 | m_height - static_cast<float>(m_background->get_height())), |
536 | layer); |
537 | context.color().draw_surface(m_background2, |
538 | Vector(static_cast<float>(context_center_x - background_center_x + m_backgroundOffset), |
539 | m_height - static_cast<float>(m_background->get_height())), |
540 | layer); |
541 | for (int x = (context_center_x - background_center_x |
542 | - (static_cast<int>(ceilf(static_cast<float>(context.get_width()) / |
543 | static_cast<float>(m_background->get_width())) - 1) * m_background->get_width())); |
544 | x < context.get_width(); |
545 | x += m_background->get_width()) |
546 | { |
547 | context.color().draw_surface(m_background, Vector(static_cast<float>(x), |
548 | m_height - static_cast<float>(m_background->get_height())), |
549 | layer); |
550 | } |
551 | |
552 | int lineNo = 0; |
553 | |
554 | if (m_focused) { |
555 | lineNo++; |
556 | float py = m_height-4-1 * m_font->get_height(); |
557 | std::string line = "> " + m_inputBuffer; |
558 | context.color().draw_text(m_font, line, Vector(4, py), ALIGN_LEFT, layer); |
559 | |
560 | if (SDL_GetTicks() % 500 < 250) { |
561 | std::string::size_type p = 2 + m_inputBufferPosition; |
562 | float cursor_x; |
563 | if (p >= line.size()) |
564 | { |
565 | cursor_x = m_font->get_text_width(line); |
566 | } |
567 | else |
568 | { |
569 | cursor_x = m_font->get_text_width(line.substr(0, p)); |
570 | } |
571 | context.color().draw_filled_rect(Rectf(Vector(3 + cursor_x, py), |
572 | Sizef(2.0f, m_font->get_height() - 2)), |
573 | Color(1.0f, 1.0f, 1.0f, 0.75f), layer); |
574 | } |
575 | } |
576 | |
577 | int skipLines = -m_offset; |
578 | for (std::list<std::string>::iterator i = m_buffer.m_lines.begin(); i != m_buffer.m_lines.end(); ++i) |
579 | { |
580 | if (skipLines-- > 0) continue; |
581 | lineNo++; |
582 | float py = static_cast<float>(m_height - 4.0f - static_cast<float>(lineNo) * m_font->get_height()); |
583 | if (py < -m_font->get_height()) break; |
584 | context.color().draw_text(m_font, *i, Vector(4.0f, py), ALIGN_LEFT, layer); |
585 | } |
586 | context.pop_transform(); |
587 | } |
588 | |
589 | ConsoleStreamBuffer ConsoleBuffer::s_outputBuffer; |
590 | std::ostream ConsoleBuffer::output(&ConsoleBuffer::s_outputBuffer); |
591 | |
592 | /* EOF */ |
593 | |