#include "charman.h" #include "images.h" #include "lastseen.h" #include "logs_utils.h" #include "render.h" #include "terminal.h" #include "utils.h" #include "zf_log.h" #include #include #include #include #include #include #include // write #include extern struct console_details console; extern std::string username; extern std::string fullname; #define BSIZE 512 void a_or_an(std::string &word) { /* If it starts with a vowel "an" */ char ch = toupper(word[0]); if ((ch == 'A') || (ch == 'E') || (ch == 'I') || (ch == 'O') || (ch == 'U')) { word.insert(0, " an "); } else { word.insert(0, " a "); } } /* * harry_idle_event(fd) * * User is idle, let's let them know we're here. HAHAHA! * */ void harry_idle_event(int fd) { // Make something happen std::ostringstream buffer; int r; int level = harry_level(); if (!level) return; // If the username is something, we have their info! bool have_userinfo = !username.empty(); int xpos = console.posx; // This is no where near finished, BUT! // Do not put any ^ codes in these -- the strlen() would be wrong. const char *phrases[] = { "Hahaha", "Snicker, snicker", "Boo!", "Arrooo!", "Ahh-wooo!", "Aaaooo!", "Sorry, I forgot!", "The Matrix has you...", "Follow the white rabbit.", }; const int total_phrases = sizeof(phrases) / sizeof(char *); const char *user_phrases[] = { "Is USER really here?", "Knock, Knock NICK.", "What is a NICK?", "Wake up, NICK...", "Here lies USER, rest in peace.", "Don't be scared, NICK.", }; const int total_user_phrases = sizeof(user_phrases) / sizeof(char *); // I'd like to use total_possible, but ... it isn't possible. (NNY) static LastSeen last_seen_harry_event(LastSeen::best_guess(total_phrases)); int total_possible = have_userinfo ? total_phrases + total_user_phrases : total_phrases; do { r = randint(total_possible); } while (last_seen_harry_event.seen_before(r)); ZF_LOGD("have %d picked %d total %d console @ %d,%d", have_userinfo, r, total_possible, console.posx, console.posy); std::string selected; if (r >= total_phrases) { selected = user_phrases[r - total_phrases]; std::string temp = username; a_or_an(temp); replace(selected, " a NICK", temp); replace(selected, "USER", fullname); replace(selected, "NICK", username); } else selected = phrases[r]; ZF_LOGD("Selected: %s", selected.c_str()); if (selected.size() + xpos > 78) { ZF_LOGD("Sorry, too long (%d)", (int)selected.size() + xpos); } else { int color = randint(15) + 1; int pause = 2; if (selected.size() > 20) { pause = 5; } // %02d = std::setfill('0') << std::setw(2) << (int) /* buffer << "^CS^S2^C" << std::setfill('0') << std::setw(2) << color << phrases[r] << "^P2^CR^D" << std::setw(2) << strlen(phrases[r]); */ buffer << TRIGGER "CS" TRIGGER "S2" TRIGGER "C" << std::setfill('0') << std::setw(2) << color << selected // Reset SPEED and RENDER << TRIGGER "S0" TRIGGER "R0" TRIGGER "P" << pause << TRIGGER "CR" TRIGGER "D" << std::setw(2) << selected.size(); std::string str = buffer.str(); ZF_LOGD("harry_event: render(%d, \"%s\")", fd, str.c_str()); render(fd, str); } } void init_harry() { // init_have_seen(last_seen_harry_event, MAX_HARRY_EVENT_DUPS); // ZF_LOGD("init => %d %d", last_seen_harry_event[0], // last_seen_harry_event[1]); console_init(&console); reset_render(); } // char words[] = "[a-zA-Z]+( [a-zA-Z]+)+"; int mangle_clrscr(std::string &buffer, std::string &work, size_t pos) { static int ANSI_CLS_count = 0; ZF_LOGI("seen: ANSI_CLS"); ANSI_CLS_count++; int level = harry_level(); if (!level) return 0; // Don't activate on the first ANSI_CLS (that's the BBS Version display/login) if (ANSI_CLS_count > 1) { // if (random_activate((level + 1) / 2)) { if (random_activate(level * 5)) { std::ostringstream display; int needs_cls = 0; struct image { const char **lines; int size; int cls; int width; // height = size } images[] = {{ghost, sizeof(ghost) / sizeof(char *), 1, 0}, {ghead, sizeof(ghead) / sizeof(char *), 1, 0}, {wolf, sizeof(wolf) / sizeof(char *), 1, 0}, {panther, sizeof(panther) / sizeof(char *), 1, 0}, {bat, sizeof(bat) / sizeof(char *), 1, 0}, {icu, sizeof(icu) / sizeof(char *), 0, 20}, {skull, sizeof(skull) / sizeof(char *), 0, 19}, {skullblink, sizeof(skullblink) / sizeof(char *), 0, 19}, // {specter, sizeof(specter) / sizeof(char *), 1, 0}, {owl, sizeof(owl) / sizeof(char *), 0, 70} }; const int total_images = sizeof(images) / sizeof(image); static LastSeen last_files(LastSeen::best_guess(total_images)); int r; do { r = randint(total_images); } while (last_files.seen_before(r)); std::string fgoto; if (!images[r].cls) { int x = 0, y = 0; x = randint(79 - images[r].width) + 1; y = randint(24 - images[r].size) + 1; display << TRIGGER "CS" TRIGGER "f" << std::setfill('0') << std::setw(2) << x << std::setw(2) << y << "\x1b[1;1H"; fgoto = display.str(); // reset display display.str(std::string()); display.clear(); } else { if (images[r].lines == specter) { // specter! fgoto.assign(TRIGGER "S1" TRIGGER "CS" TRIGGER "F"); } else fgoto.assign(TRIGGER "CS" TRIGGER "F"); } needs_cls = images[r].cls; // I get what's happening. Mystic moves cursor to home, CLS, cursor // home. When we get here, we're ALWAYS at the top of the screen... // Hence our bat isn't displayed at the end of the screen. // This is before the actual CLS, so we CLS before displaying our files. // I tried a ^P2 before doing this .. but I'd rather have the picture up // right away I think. // Ok, yes, there's no filename being sent. :P render_image(images[r].lines, images[r].size); display << (needs_cls ? "\x1b[2J" : "") << fgoto << TRIGGER "S0" TRIGGER "CR" TRIGGER "P3"; std::string display_output = display.str(); ZF_LOGI("mangle(ANSI_CLS): %d file inserted %s", r, repr(display_output.c_str())); // Move the buffer so there's room for the display string. buffer.insert(pos, display_output); work.insert(pos, std::string(display_output.size(), ' ')); return 1; // need_render = 1; } else { // if (random_activate((level + 1) / 2)) { if (random_activate(level * 5)) { int r; std::ostringstream display; // If the username is something, we have their info! bool have_userinfo = !username.empty(); const char *phrasing[] = { TRIGGER "R1Haha" TRIGGER "P1ha" TRIGGER "P1ha", "Poof!", "Anyone there?", TRIGGER "R1Knock, " TRIGGER "P1Knock", /* This picks random color and position -- then homes cursor and changes to another color. (This can be seen.) */ TRIGGER "G0101" TRIGGER "C07" TRIGGER "S0" "Segmentation fault" TRIGGER "P1" " (core dumped)" TRIGGER "P2", TRIGGER "G0101" TRIGGER "C07" TRIGGER "S0" "/usr/include/c++/7/bits/basic_string.h:1057: " "std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference " "std::__cxx11::basic_string<_CharT, _Traits, " "_Alloc>::operator[](std::__cxx11::basic_string<_CharT, _Traits, " "_Alloc>::size_type) [with _CharT = char; _Traits = " "std::char_traits; _Alloc = std::allocator; " "std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference = " "char&; std::__cxx11::basic_string<_CharT, _Traits, " "_Alloc>::size_type = long unsigned int]: Assertion '__pos <= " "size()' failed." TRIGGER "P1" "\r\nAborted (core dumped)" TRIGGER "P2" TRIGGER "S0", TRIGGER "G0101" TRIGGER "C07" TRIGGER "S0" "/usr/include/c++/7/debug/vector:417:\r\n" "Error: attempt to subscript container with out-of-bounds " "index 13, but\r\n" "container only holds 0 elements.\r\n" "\r\n" "Objects involved in the operation:\r\n" " sequence \"this\" @ 0x0x7fff43fa94a0 {\r\n" " type = " "std::__debug::vector, std::allocator >, " "std::allocator, std::allocator > > >;\r\n" " }\r\n" "Aborted (core dumped)" TRIGGER "P2" TRIGGER "S0" }; // the Vector error could use some randomness in the element accessed, // as well as how many elements and the memory location. const int max_phrases = sizeof(phrasing) / sizeof(char *); const char *user_phrasing[] = { "We've got you now, NICK!", "Are you there, USER?", TRIGGER "R1Knock, " TRIGGER "P1Knock " TRIGGER "P1NICK", "I see you," TRIGGER "P1" TRIGGER "S1" TRIGGER "R1 NICK" TRIGGER "S0" TRIGGER "R0", "I see NICK hiding." }; const int max_user_phrases = sizeof(user_phrasing) / sizeof(char *); static LastSeen last_phrasing(LastSeen::best_guess(max_phrases)); int total_possible = have_userinfo ? max_phrases + max_user_phrases : max_phrases; ZF_LOGI("mangle(ANSI_CLS)"); do { r = randint(total_possible); } while (last_phrasing.seen_before(r)); std::string selected; if (r >= max_phrases) { selected = user_phrasing[r - max_phrases]; std::string temp = username; a_or_an(temp); replace(selected, " a NICK", temp); replace(selected, "USER", fullname); replace(selected, "NICK", username); } else selected = phrasing[r]; int color = randint(14) + 1; int x = randint(30) + 1; int y = randint(15) + 1; /* Don't have it pause there before moving the cursor. Move the cursor, get the color changed, THEN pause. Then act all crazy. NOTE: Make sure if you use any ^R Render effects, turn them off before trying to display the restore_color. :P ^R0 Also, make sure you re-home the cursor ^G0101 because that's where they are expecting the cursor to be! (At least it's how Mystic does it.) HOME, CLS, HOME, ... Not sure what others do there. We'll see. */ if (strncmp(selected.c_str(), TRIGGER "G", 2) == 0) { display << TRIGGER "CS" TRIGGER "S3" TRIGGER "P1" << selected << TRIGGER "S0" TRIGGER "R0" TRIGGER "CR" TRIGGER "P1" TRIGGER "G0101"; // This starts with a GOTO, so don't use our random position } else { display << TRIGGER "CS" TRIGGER "G" << std::setw(2) << std::setfill('0') << x << std::setw(2) << y << TRIGGER "S3" TRIGGER "C" << std::setw(2) << color << TRIGGER "P1" << selected << TRIGGER "S0" TRIGGER "R0" TRIGGER "CR" TRIGGER "P1" TRIGGER "G0101"; }; std::string display_output = display.str(); // Added debug statement so we can identify what was sent... color, // number picked and what that is ZF_LOGD("mangle(ANSI_CLS): Inserted color=%02d r=%d phrase='%s'", color, r, phrasing[r]); ZF_LOGI("mangle(ANSI_CLS): %d %s", r, repr(display_output.c_str())); // Move the buffer so there's room for the display string. buffer.insert(pos, display_output); work.insert(pos, std::string(display_output.size(), ' ')); return 1; // need_render = 1; } } } return 0; } int mangle(int fd, std::string &buffer) { // a simple default for now. // ZF_LOGV("mangle [%s]", logrepr(buffer.c_str())); ZF_LOGV_LR("mangle:", buffer); /* ZF_LOGV_MEM(buffer.data(), buffer.size(), "mangle(%d): %lu bytes", fd, buffer.size()); */ int need_render = 0; static std::string work; static size_t work_size = 0; work.assign(buffer); // This should allow us to monitor any memory allocations if (work.capacity() != work_size) { ZF_LOGD("work cap %lu -> %lu", work_size, work.capacity()); work_size = work.capacity(); } int level = harry_level(); std::ostringstream new_buffer; std::smatch match; if (level) { // Strings are good, but Regex is better // Mystic BBS v1.12 A43 for Linux Node 1 // Mystic BBS Version 1.12 A45 static int bbs_match = 0; if (!bbs_match) { std::regex bbs_what( "(?:Mystic BBS Version [0-9.]+ A[0-9]+)|(?:Mystic BBS " "v[0-9.]+ A[0-9]+ for Linux Node [0-9]+)"); // std::regex bbs_what("Mystic BBS Version [0-9.]+ A[0-9]+"); // std::regex_constants::ECMAScript); // Mystic BBS v[0-9.]+ A[0-9]+ for Linux Node [0-9]+ if (std::regex_search(buffer, match, bbs_what)) { // We have a match ZF_LOGD("bbs_seen"); bbs_match = 1; std::string old_string = buffer.substr(match.position(0), match.length(0)); // Build a new and better string std::string new_string; const char *bbs_systems[] = { "Haunted BBS", "Harry's BBS", "Scary BBS Software", "Screaming BBS", "Fright BBS", "Gravestone BBS", }; const char *operating_systems[] = { "OS/360", "CP/M", "OS/9", "Xenix", "MS-DOS", "PC-DOS", "DR-DOS", "QNX", "Novell Netware", "AmigaOS", "Windows NT", "Windows CE", "AIX", "OS/2", "OS/400", "NeXTSTEP", "MINIX", "Solaris", "Plan 9", "FreeBSD", "Windows 95", "Palm OS", "Mac OS X", "Windows XP", "DESQview", "EMACS", // EMACS *should* be an OS! }; int r = randint(sizeof(bbs_systems) / sizeof(char *)); new_buffer << bbs_systems[r] << " v" << randint(10) << "." << randint(80); new_buffer << " for "; r = randint(sizeof(operating_systems) / sizeof(char *)); new_buffer << operating_systems[r] << " Node " << randint(150 * level); new_string = new_buffer.str(); // reset buffer new_buffer.str(std::string()); new_buffer.clear(); replace(buffer, old_string, new_string); replace(work, old_string, new_string); level = 0; // temp turn off the manglers! ;) } } static int author_match = 0; if (!author_match) { static std::regex author("Copyright \\(C\\) [0-9-]+ By James Coyle"); if (std::regex_search(buffer, match, author)) { // We have a match ZF_LOGD("author seen"); author_match = 1; std::string old_author = buffer.substr(match.position(0), match.length(0)); // Build a new and better string const char *coder_names[] = {"Horrible Harry", "Ghost Writer", "Sands of Time", "Spector Software", "Creepy Coder"}; if (randint(10) < 5) new_buffer << "Copyfright "; else new_buffer << "Copyright "; new_buffer << "(C) " << 1000 + randint(999) << "-" << randint(24000) << " By "; int r = randint(sizeof(coder_names) / sizeof(char *)); new_buffer << coder_names[r]; std::string new_author = new_buffer.str(); replace(buffer, old_author, new_author); replace(work, old_author, new_author); level = 0; } } } const char *ANSI_CLS = "\x1b[2J"; size_t pos = buffer.find(ANSI_CLS); if (pos != std::string::npos) { if (level) if (mangle_clrscr(buffer, work, pos)) { need_render = 1; } } static std::string text; static std::vector text_offsets; size_t stri; text.clear(); text_offsets.clear(); // Don't use console, this for just spliting text and ansi into different // buffers. console_details chew = console; for (stri = 0; stri < buffer.size(); ++stri) { termchar tc = console_char(&chew, work[stri]); if (tc.in_ansi) { if (tc.ansi != START) { // Ok, this is something. What is it? // ZF_LOGV("ANSI type %d at %lu", tc.ansi, stri); switch (tc.ansi) { case CURSOR: case CLEAR: case OTHER: text.append(1, '.'); text_offsets.push_back(-1); break; case START: case COLOR: // text.append(1, ' '); // text_offsets.push_back(-1); // color changes show as nothing in the text string. break; } } } else { // These should never get out of sync ... if (text.size() != text_offsets.size()) { ZF_LOGE("Error: text != text_offsets %lu != %lu", text.size(), text_offsets.size()); } text.append(1, work[stri]); text_offsets.push_back(stri); } } // Control buffer debugging output. #ifndef NO_BUFFER_DEBUG // ZF_LOGV_LR("mangle:", buffer); ZF_LOGV_LR("Buffer:", buffer); ZF_LOGV_LR("Work:", work); ZF_LOGV_LR("Text:", text); // ZF_LOGV("Buffer: %s", logrepr(buffer.c_str())); // ZF_LOGV("Work: %s", logrepr(work.c_str())); // ZF_LOGV("Text: %s", logrepr(text.c_str())); // ZF_LOGV_MEM(buffer.data(), buffer.size(), "Buffer:"); // ZF_LOGV_MEM(work.data(), work.size(), "Work:"); // ZF_LOGV_MEM(text.data(), text.size(), "Text Buffer:"); // Output vector contents std::ostringstream oss; int comma = 0; for (auto it = std::begin(text_offsets); it != std::end(text_offsets); ++it) { if (comma) { oss << ", "; }; comma++; oss << *it; if (comma == 30) { std::string temp_output = oss.str(); ZF_LOGV("Vector: %s", temp_output.c_str()); // reset ostringstream oss.str(std::string()); oss.clear(); comma = 0; } } std::string vector_output = oss.str(); ZF_LOGV("Vector: %s", vector_output.c_str()); // reset oss (if we need it) oss.str(std::string()); oss.clear(); #endif // Begin the mangle process 2.0 if (level) { ZF_LOGD("CharMan"); CharMan cm(buffer, work, text, text_offsets); ZF_LOGD("CharMan %d, %d chars, render %d", cm.mangle_count, cm.mangle_chars, cm.need_render); if (cm.need_render) need_render = 1; }; if (need_render) { render(fd, buffer); } else { write(fd, buffer.data(), buffer.size()); console_receive(&console, buffer); } return need_render; }