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
31static const float FADE_SPEED = 1;
32
33ConsoleBuffer::ConsoleBuffer() :
34 m_lines(),
35 m_console(nullptr)
36{
37}
38
39void
40ConsoleBuffer::set_console(Console* console)
41{
42 assert((console && !m_console) ||
43 (m_console && !console));
44
45 m_console = console;
46}
47
48void
49ConsoleBuffer::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
59void
60ConsoleBuffer::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
88void
89ConsoleBuffer::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
106Console::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
127Console::~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
136void
137Console::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
153void
154Console::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
189void
190Console::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
217void
218Console::input(char c)
219{
220 m_inputBuffer.insert(m_inputBufferPosition, 1, c);
221 m_inputBufferPosition++;
222}
223
224void
225Console::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
233void
234Console::eraseChar()
235{
236 if (m_inputBufferPosition < static_cast<int>(m_inputBuffer.length())) {
237 m_inputBuffer.erase(m_inputBufferPosition, 1);
238 }
239}
240
241void
242Console::enter()
243{
244 m_buffer.addLines("> " + m_inputBuffer);
245 parse(m_inputBuffer);
246 m_inputBuffer = "";
247 m_inputBufferPosition = 0;
248}
249
250void
251Console::scroll(int numLines)
252{
253 m_offset += numLines;
254 if (m_offset > 0) m_offset = 0;
255}
256
257void
258Console::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
277void
278Console::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
289namespace {
290
291void 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 */
298void
299sq_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 */
338void
339sq_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
352void
353Console::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
416void
417Console::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
451bool
452Console::consoleCommand(const std::string& /*command*/, const std::vector<std::string>& /*arguments*/)
453{
454 return false;
455}
456
457bool
458Console::hasFocus() const
459{
460 return m_focused;
461}
462
463void
464Console::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
474void
475Console::open()
476{
477 if (m_stayOpen < 2)
478 m_stayOpen += 1.5f;
479}
480
481void
482Console::hide()
483{
484 m_focused = false;
485 m_height = 0;
486 m_stayOpen = 0;
487
488 // clear input buffer
489}
490
491void
492Console::toggle()
493{
494 if (Console::hasFocus()) {
495 Console::hide();
496 }
497 else {
498 Console::show();
499 }
500}
501
502void
503Console::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
521void
522Console::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
589ConsoleStreamBuffer ConsoleBuffer::s_outputBuffer;
590std::ostream ConsoleBuffer::output(&ConsoleBuffer::s_outputBuffer);
591
592/* EOF */
593