/* This file is part of Bolixo. Bolixo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Bolixo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Bolixo. If not, see . */ /* Documents and games manager. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "filesystem.h" #include "bolixo.h" #include "bolixo.m" #define INSTRUMENT_DONOTOPEN #include "instrument.h" #include #include "documentd.h" #include "documentd_menu.h" using namespace std; static DEBUG_KEY D_PROTO ("proto","Protocol information"); enum CONNECT_TYPE { TYPE_NONE, TYPE_CONTROL, TYPE_CLIENT, TYPE_SUBPROGRAM}; // Load and save may be spread over several calls // We save the gameid to make sure // This will be needed when loadmore is implemented struct LOADSAVE{ vector lines; }; struct HANDLE_INFO: public ARRAY_OBJ{ CONNECT_TYPE type; REQUEST_INFO req; map bufs; // Used to handle save and load request from bod // The string is the gameid HANDLE_INFO(){ type = TYPE_NONE; } }; DOC_WRITER::DOC_WRITER(FILE *_fout): fout(_fout) { } int DOC_WRITER::write (const char *buf, unsigned len) { int ret = -1; if (fout != nullptr){ ret = fwrite (buf,1,len,fout); }else{ FILE *f = fopen ("/tmp/save.log","a"); fprintf (f,"%s",buf); fclose (f); lines.push_back(string(buf,len)); ret = len; } return ret; } int DOC_WRITER::write (const string &l) { return write (l.c_str(),l.size()); } BOB_TYPE DOC_WRITER::getcontent () const { size_t size = 0; for (auto &s:lines) size += s.size(); BOB_TYPE ret (size); char *buf = (char*)ret.getbuffer(); for (auto &s:lines){ memcpy(buf,s.c_str(),s.size()); buf += s.size(); } return ret; } void fflush (DOC_WRITER *){} char *fgets(char *s, int size, DOC_READER *r) { char *ret = nullptr; if (r->fin != nullptr){ ret = fgets(s,size,r->fin); }else if (r->bufptr != nullptr){ const char *end = r->bufptr; if (*end != '\0'){ while (*end != '\0' && *end != '\n') end++; int len = end - r->bufptr; if (len < size){ ret = s; memcpy (s,r->bufptr,len); s[len] = '\0'; } if (*end == '\n') end++; r->bufptr = end; } } return ret; } #include "proto/bod_client.protodef" #include "proto/documentd_control.protoh" #define documentd_client_rep_waitevent_NEEDED #include "proto/documentd_client.protoh" static string documentd_path (const char *name) { return string_f("%s/bo-games/%s",getenv("HOME"),name); } void documentd_error (vector &res, PARAM_STRING s) { VARVAL v; v.var = VAR_ERROR; v.val = s.ptr; res.push_back(v); } void documentd_forcerefresh (vector &res) { VARVAL v; v.var = VAR_REFRESH; res.push_back(v); } void documentd_setchanges (vector &res) { VARVAL v; v.var = VAR_CHANGES; res.push_back(v); } /* Escape ' in a string and \. This is used to encode string as javascript statements. */ string documentd_escape(PARAM_STRING msg) { return copystring (msg,[](auto &c, auto pt){ if (is_any_of(*pt,'\'','\\')) c.insert ('\\'); }); } void documentd_setfocus (VARVAL &script_var, PARAM_STRING id) { script_var.val += "window.setTimeout(function() {\n"; script_var.val += string_f("document.getElementById('%s').focus();\n",id.ptr); script_var.val += "},0);\n"; } void documentd_button_start(string &lines, const string &gameid) { lines += string_f("
\n",gameid.c_str()); } void documentd_button_end(string &lines) { lines += "
\n"; } /* Add a space between buttons */ void documentd_button_space(string &lines) { lines += "   "; } void documentd_button_label (string &lines, PARAM_STRING txt) { lines += string_f("
%s
\n",txt.ptr); } void documentd_button (string &lines, unsigned command, PARAM_STRING txt, bool highlit) { lines += " "; lines += "
\n"; lines += string_f("
%s
\n" ,command,command,highlit ? "background-color:lightblue;" : "",txt.ptr); lines += "
\n"; } void documentd_button (string &lines, unsigned command, PARAM_STRING txt, const DOC_BUTTON_SPECS &specs, bool highlit) { lines += " "; lines += string_f("
%s
\n" ,command,command,highlit ? "lightblue" : "lightgray" ,specs.radius,specs.width,specs.margin_left,specs.margin_top,specs.margin_bottom,txt.ptr); } // Replace ' by ' string documentd_escape_html(PARAM_STRING s) { return copystring (s,[](auto &c, auto pt){ if (*pt == '\''){ c.replace(1,"'"); }else if (*pt == '\\'){ c.replace(1,"\"); }else if (*pt == '<'){ c.replace(1,"<"); } }); } /* Small button for a menu bar */ void documentd_bar_button (string &lines, unsigned command, PARAM_STRING txt, const DOC_BUTTON_SPECS &specs, bool highlit, PARAM_STRING title) { lines += " "; string tmptitle; if (title.ptr != nullptr) tmptitle = string_f("title='%s'",documentd_escape_html(title).c_str()); lines += string_f("
%s
\n" ,tmptitle.c_str(),command,command,highlit ? "lightblue" : "lightgray" ,specs.radius,specs.width,specs.margin_left,specs.margin_top,specs.margin_bottom,txt.ptr); } void documentd_bar_button (string &lines, unsigned command, PARAM_STRING txt, const DOC_BUTTON_SPECS &specs, bool highlit) { documentd_bar_button(lines, command, txt, specs, highlit,nullptr); } // A chat line starts with the user followed by : followed by some user content // So we put the user in bold and escape the content static void formatchatline (time_t now, PARAM_STRING line, string ¬ify) { const char *pt = strchr(line.ptr,':'); if (pt != nullptr){ string user = string(line.ptr,pt-line.ptr); pt++; notify += string_f("appendchat(%ld,'%s: %s');\n",now,documentd_escape_html(user).c_str(),documentd_escape_html(pt).c_str()); } } /* Create a small one line chat */ void documentd_chat(string &lines, PARAM_STRING username, bool mobile, const vector &content, unsigned width, unsigned height) { lines += string_f("
" ,width,height); lines += "
\n";; lines += string_f("\n" ,width,MSG_U(I_CHAT,"Chat")); lines += "\n"; } // r.val is a list of fields. Each field has a name and a value on a single text line. // name:value\nname:value\n void documentd_parsefields (const char *val, vector &fields) { vectortb; str_splitline (val,'\n',tb); for (auto &l:tb){ const char *start = l.c_str(); const char *pt = strchr(start,':'); if (pt != nullptr){ VARVAL var; var.var = string(start,pt-start); var.val = pt+1; fields.emplace_back(move(var)); } } } GAME::~GAME() { for (auto &n:notification_fds) close (n.first); } void GAME::appendchat(PARAM_STRING line, string ¬ify) { time_t now = time(nullptr); chat.emplace_back(now,line.ptr); while (chat.size() > 20) chat.erase(chat.begin()); formatchatline (now,line,notify); } void GAME::appendchat(PARAM_STRING line, string ¬ify, vector &res, const DOC_CONTEXT &ctx) { appendchat(line,notify); documentd_setchanges(res); setmodified (ctx.username); } static size_t total_notify_size=0; void GAME::add_notification (PARAM_STRING script, const vector &unotifies) { if (script.ptr[0] != '\0' || unotifies.size() > 0){ if (script.ptr[0] != '\0') notifications.emplace_back(script,sequence); size_t len_script = strlen(script.ptr); for (auto &n:notification_fds){ total_notify_size += len_script; string un_script; for (auto &un:unotifies){ if (find(un.connections.begin(),un.connections.end(),n.first)!=un.connections.end()){ if (un_script.empty()) un_script = script.ptr; un_script += un.val; } } if (un_script.size() != 0 || len_script != 0){ const char *pt_script = un_script.size() > 0 ? un_script.c_str() : script.ptr; documentd_client_rep_waitevent(n.first,true,"",pt_script,sequence); } } sequence++; // We keep around the last 10 notifications while (notifications.size() > 10) notifications.erase(notifications.begin()); } } void GAME::add_notification (PARAM_STRING script) { vector unotifies; add_notification(script,unotifies); } void GAME::add_notification_fd(int fd, const char *username, const char *connectid) { notification_fds[fd] = FD_INFO(username,connectid); } int GAME::del_notification_fd(int fd) { return notification_fds.erase(fd) > 0 ? 0 : -1; } /* Returnt the list of users waiting for event (for that game/document) */ std::set GAME::get_waiting_users() { set ret; for (auto &n:notification_fds){ ret.insert (n.second.username); } return ret; } static const char *nodename = nullptr; const char *documentd_getnodename() { const char *ret = nodename; const char *pt; if (is_start_any_ofnc(nodename,pt,"http://","https://")){ ret = pt; } return ret; } std::string GAME::format_draw_waiting (const std::set &waitings) { string line; line = "draw_waiting(["; const char *sep = ""; for (auto &w:waitings){ if (w.find('@')==string::npos){ const char *pt; if (is_start_any_ofnc(nodename,pt,"http://","https://")){ line += string_f("%s'%s@%s'",sep,w.c_str(),pt); } }else{ line += string_f("%s'%s'",sep,w.c_str()); } sep = ","; } line += "]);\n"; //tlmp_warning ("line=%s\n",line.c_str()); return line; } void GAME::draw_waiting_users(string &lines, unsigned width, unsigned height, const char *style) { lines += string_f("
\n" ,gameid.c_str(),style,width,height); lines += "
\n"; lines += "\n"; has_waiting_users = true; } /* Return true if this username is part of the last update_waiting_user() call. */ bool GAME::waiting_user(const char *username) { return last_waitings.count(username) > 0; } /* Format the update if needed (because the connected users list has changed */ void GAME::update_waiting_users(string &lines) { if (has_waiting_users){ auto waitings = get_waiting_users(); if (waitings != last_waitings){ // tlmp_warning ("waiting != last_waitings"); lines += format_draw_waiting(waitings); last_waitings = waitings; } } } const char *GAME::locate_event (unsigned &sequence) { const char *ret = nullptr; for (auto &n:notifications){ if (n.sequence > sequence){ sequence = n.sequence; ret = n.script.c_str(); break; } } return ret; } void GAME::update_msg( bool to_all, // The message will be shown to all player or not PARAM_STRING msg, const char *color, vector &res) { VARVAL mvar; mvar.var = to_all ? VAR_NOTIFY : VAR_SCRIPT; mvar.val += string_f("%s_updmsg('%s','%s');\n",getclass(),color,documentd_escape(msg).c_str()); res.emplace_back(mvar); } void GAME::manyexec ( const std::vector &steps, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, std::vector &res, std::vector &unotifies) { for (auto &v:steps){ exec(v.var,v.val,ctx,sp,res,unotifies); } } void GAME::resetgame() { } void GAME::testwin(vector &res) { } void GAME::engine_reply(const char *line, string ¬ify, bool &done) { done = true; } /* Get the list of documents/games embedded in this document */ set GAME::get_embed_list() const { set ret; auto tb = str_splitline (gameid,'/'); if (tb.size() >= 4){ string docprj = string_f("/%s/%s/%s/",tb[1].c_str(),tb[2].c_str(),tb[3].c_str()); auto embs = get_embed_specs(); for (auto &e:embs){ ret.insert(docprj+e.document); } } return ret; } void GAME::set_embed_options (const DOCUMENT_EMBED &embed, bool not_square) { } vector GAME::get_embed_specs() const { vector ret; return ret; } void GAME::remove_session(const char *) { } static bool protocol_stats=false; // produce a tlmp_warning showing how much is sent back static size_t total_content_size = 0; static size_t total_script_size = 0; static size_t total_unotify_size = 0; static unsigned total_requests=0; static map *pt_games = nullptr; /* Send the content of val to all waiting connections. If the list of waiting users has changed, send notifications as well. */ static void documentd_send_notifies(GAME_P game, vector &embeds, string &val, const vector &unotifies, size_t ¬ify_size) { // Notifications are sent using the waitevent system // Check if the connected users list must be updated. game->update_waiting_users(val); if (val.size() > 0 || unotifies.size() >0){ notify_size += val.size(); game->add_notification(val,unotifies); for (auto e:embeds) e->add_notification(val,unotifies); } } static bool documentd_playstep ( vector &programs, map &games, const char *gameid, const vector &steps, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, vector &res, string &msg, bool &unknown) { bool success = false; unknown = false; pt_games = &games; auto g = games.find(gameid); if (g != games.end()){ success = true; GAME_P game = g->second; vector unotifies; game->manyexec(steps,ctx,sp,res,unotifies); game->testwin(res); size_t content_size = 0; size_t notify_size = 0; size_t script_size = 0; // Find games embeding this part of this game vector embeds; for (auto &gg:games){ auto tb = gg.second->get_embed_list(); if (tb.count(g->first) > 0){ embeds.push_back(gg.second); } } bool notify_seen = false; for (auto r = res.begin(); r != res.end(); ){ if (r->var == VAR_NOTIFY){ notify_seen = true; documentd_send_notifies (game,embeds,r->val,unotifies,notify_size); r = res.erase(r); }else if (r->var == VAR_SCRIPT){ script_size += r->val.size(); r++; }else if (r->var == VAR_CONTENT){ content_size += r->val.size(); r->val += "\n"; r++; }else if (r->var == VAR_ENGINE){ // We send commands to the engine, and removes it. bool found = false; for (auto &p:programs){ if (p.is_class(game->getclass())){ p.send(gameid,r->val); found = true; break; } } if (!found){ tlmp_error ("No engine for game type %s, gameid=%s",game->getclass(),gameid); } r = res.erase(r); }else{ r++; } } if (!notify_seen){ string val; documentd_send_notifies (game,embeds,val,unotifies,notify_size); } if (protocol_stats) tlmp_warning ("protocol content_size=%zu script_size=%zu notify_size=%zu",content_size,script_size,notify_size); total_content_size += content_size; total_script_size += script_size; total_unotify_size += notify_size; total_requests++; }else{ msg = "Unknown game id"; unknown = true; } pt_games = nullptr; return success; } static GAME_P documentd_newgame(const char *type, const char *gameid, string &msg) { GAME_P p = nullptr; if (strncmp(type,"boBOTICT",8)==0){ p = make_TICTACTO(); }else if (strncmp(type,"boBOSUDO",8)==0){ p = make_SUDOKU(); }else if (strncmp(type,"boBOWORD",8)==0){ p = make_WORDPROC(); }else if (strncmp(type,"boBOWHIT",8)==0){ p = make_WHITEBOARD(); }else if (strncmp(type,"boBOCHEC",8)==0){ p = make_CHECKERS(); }else if (strncmp(type,"boBOCHES",8)==0){ p = make_CHESS(); }else if (strncmp(type,"boBOPHOT",8)==0){ p = make_PHOTOS(); }else if (strncmp(type,"boBOCALC",8)==0){ p = make_CALC(); }else if (strncmp(type,"boBOVIDC",8)==0){ p = make_VIDCONF(); }else{ msg = "Unknown game or document type"; } if (p != nullptr) p->setgameid(gameid); return p; } static bool documentd_startgame( map &games, const char *gamename, const char *gameid, string &msg) { bool ret = false; auto g = games.find(gameid); if (g != games.end()){ msg = "Game already exist"; }else{ GAME_P p = documentd_newgame(gamename,gameid,msg); if (p != nullptr){ games[gameid] = p; p->resetgame(); ret = true; } } return ret; } /* Create a popup window displaying a message in the middle of a document using javascript */ static void documentd_create_popup(PARAM_STRING msg, const GAME_P doc) { static const char *tb[][2]={ {"CHEC","doc_checkers"}, {"CHES","doc_chess"}, {"SUDO","doc_sudoku"}, {"WORD","doc_wordproc"}, {"WHIT","doc_whiteboard"}, {"TICT","doc_tictacto"}, {"CALC","doc_calc"}, {"PHOT","doc_photos"}, {"VIDC","doc_vidconf"}, }; const char *type = doc->getclass(); const char *docid=nullptr; for (auto &t:tb){ if (strcmp(t[0],type)==0){ docid=t[1]; break; } } if (docid == nullptr){ tlmp_error ("documentd_create_popup: no docid for class=%s",type); }else{ string script; script = "let div = document.createElement(\"div\");\n" "console.log ('div='+div);\n" "div.setAttribute(\"class\", \"popup\");\n" "div.innerHTML=\"" "" "\";\n" "document.body.appendChild(div);\n" "var pop=document.getElementById('popup');\n" "pop.classList.toggle('show');\n"; script += string_f("var doc=document.getElementById('%s');\n",docid); script += "if (doc == null){\n"; script += string_f("\tdoc=document.getElementById('full%s');\n",docid); script += "}\n"; script += "var rectpop = pop.getBoundingClientRect();\n"; script += "var rectdoc = doc.getBoundingClientRect();\n"; script += "pop.style.left=rectdoc.left+(rectdoc.right-rectdoc.left)/2-(rectpop.right-rectpop.left)/2;\n" "pop.style.top=rectdoc.top+(rectdoc.bottom-rectdoc.top)/2;\n"; doc->add_notification (script); } } /* Remove a game if possible (force overrides everything). The revision must match. It is generally obtained using the method is_modified or listgames. No user must be waiting for the game. The game must not be embedded in another game/document. */ static bool documentd_endgame ( map &games, const char *gameid, const char *username, unsigned revision, bool force, string &msg) { bool ret = false; auto g = games.find(gameid); if (g != games.end()){ if (g->second->get_revision() != revision){ msg = "Revision mismatch"; }else if (!force && g->second->get_nbwait() > 0){ msg = "User waitings"; }else{ ret = true; if (!force){ for (auto &gg:games){ if (gg.second != g->second){ auto tb = gg.second->get_embed_list(); if (tb.count(gameid)>0){ ret = false; msg = string_f("game is embedded in document %s",gg.first.c_str()); break; } } } } if (ret){ string msg = string_f(MSG_U(I_DELETED,"
Attention

" "The document has been deleted by %s
" "

" "The document may be undeleted if needed"),username); documentd_create_popup (msg,g->second); games.erase (g); } } }else{ msg = "Unknown game id"; } return ret; } static void closetb(int tb[2]) { close (tb[0]); close (tb[1]); } static void forgettb(int tb[2]) { tb[0] = tb[1] = -1; } static void dup_close (int fd, int target_fd) { dup2 (fd,target_fd); close (fd); } /* Convert a relative path (relative to the project) to absolute path. If the relative path starts with a /, it means it is a path relative to the project. If not, it is relative to the folder holding the gameid. The project is extracted from the gameid. */ string documentd_rel2abs (PARAM_STRING gameid, PARAM_STRING relpath) { string ret; auto tb = str_splitline (gameid.ptr,'/'); if (tb.size() >= 4){ ret = string_f("/%s/%s/%s",tb[1].c_str(),tb[2].c_str(),tb[3].c_str()); if (relpath.ptr[0] == '/'){ ret += relpath.ptr; }else{ for (unsigned i=4; i 0){ auto g = pt_games->find(docpath); if (g != pt_games->end()){ auto game = g->second; vector steps; VARVAL_receive var; var.var = command.ptr; var.val = option.ptr; steps.push_back(var); vector res; DOC_CONTEXT ctx; ctx.session = ""; ctx.username = ""; ctx.maywrite = true; ctx.docnum = docnum; vector unotifies; game->manyexec(steps,ctx,sp,res,unotifies); for (auto &r:res){ if (is_any_of(r.var,VAR_CONTENT,VAR_STYLES)){ ret = move(r.val); }else if (is_any_of(r.var,VAR_DEFSCRIPT)){ script = move(r.val); } } } } return ret; } static GAME_P documentd_findsubgame(const char *gameid, const string &document) { GAME_P ret; string docpath = documentd_rel2abs(gameid,document); if (docpath.size() > 0){ auto p = pt_games->find(docpath); if (p != pt_games->end()){ ret = p->second; } } return ret; } /* Get all the scripts, styles and templates to support document embedding in this document */ void documentd_imbeds ( GAME *game, string &lines, const DOC_UI_SPECS_receive &sp) { set type_seens; // Document types already processed auto tb = game->get_embed_specs(); type_seens.insert(game->getclass()); // A document may imbed another document of the same // type, so no need to source the styles and scripts for (auto &t:tb){ string docpath = documentd_rel2abs(game->get_gameid(),t.document); auto p = pt_games->find(docpath); if (p != pt_games->end()){ GAME_P sub = p->second; vector steps; VARVAL_receive var; if (type_seens.insert(sub->getclass()).second){ // We only need those once per document type var.var = REQ_FUNCTIONS; steps.push_back(var); var.var = REQ_STYLES; steps.push_back(var); } var.var = REQ_REGION; var.val = t.region.c_str(); steps.push_back(var); vector res; DOC_CONTEXT ctx; ctx.docnum = t.docnum; vector unotifies; sub->manyexec(steps,ctx,sp,res,unotifies); for (auto &r:res){ if (r.var == VAR_STYLES){ lines += "\n"; }else if (r.var == VAR_DEFSCRIPT){ lines += "\n"; }else if (r.var == VAR_CONTENT){ lines += string_f("\n"; game->set_embed_options(t,r.val.find("not-square")!=string::npos); } } } } } /* Insert a new imbedded document into a document currently displayed. We are using this function when a user is adding a new document. */ void documentd_insert_imbed( GAME *game, VARVAL ¬ify_var, DOCUMENT_EMBED &imbed, const DOC_UI_SPECS_receive &sp) { // We have to know if another document of the same type is currently imbedded. // Potentially, the parent document is imbedding a sub-document of the same type. const char *gameid = game->get_gameid(); bool found = false; auto newsub = documentd_findsubgame(gameid,imbed.document); if (newsub == nullptr){ tlmp_error ("documentd_insert_imbed: unknown document %s\n",imbed.document.c_str()); return; } if (is_eq(game->getclass(),newsub->getclass())){ found = true; }else{ auto tb = game->get_embed_specs(); for (auto &t:tb){ auto sub = documentd_findsubgame(gameid,t.document); if (is_eq(sub->getclass(),newsub->getclass())){ found = true; break; } } } string script; //tlmp_warning ("insert_imbed process functions and styles: %s %s found=%d\n",gameid,imbed.document.c_str(),found); if (!found){ documentd_imbed (gameid,imbed.document,REQ_FUNCTIONS,"",imbed.docnum,sp,script); notify_var.val += script; string content = documentd_imbed (gameid,imbed.document,REQ_STYLES,"",imbed.docnum,sp,script); notify_var.val += "document.body.innerHTML+=`"; notify_var.val += "`;\n"; script.clear(); } string content = documentd_imbed (gameid,imbed.document,REQ_REGION,imbed.region,imbed.docnum,sp,script); // First we add the sub-document template definition, then the scripts notify_var.val += "document.body.innerHTML+=`"; notify_var.val += string_f("`;\n"; notify_var.val += script; if (content.find("not-square")!=string::npos) imbed.not_square = true; } /* Generate the function to loop inside all javascript object, then search for an id and apply a function */ string documentd_js_loop_function(const char *board_prefix, const char *prefix, int docnum) { string lines; if (docnum == 0){ lines += "var doc_lst=[];\n"; lines += "var doc_cur_gameid='';\n"; lines += "function gen_loop_findid(svg,tag,id,chs,fct){\n" "\tif (svg != null){\n" "\t\tvar elms=svg.getElementsByTagName(tag);\n" "\t\tvar e_found = null;\n" "\t\tvar min_depth = 1000;\n" // We have to find the element closer to the parent. // With embeded/linked document, we may have several elements // with the same id. "\t\tfor (var j=0; j tb; str_splitline (_command.ptr,' ',tb); const char *tbp[tb.size()+1]; for (unsigned i=0; i(n)); } SUBPROGRAM &SUBPROGRAM::operator =(SUBPROGRAM &&n) { if (this != &n){ subswap(forward(n)); } return *this; } SUBPROGRAM::~SUBPROGRAM() { //printf ("pid=%d fdin=%d fdout=%d fderr=%d command=%s\n",(int)pid,fdin,fdout,fderr,command.c_str()); close (fdin); close (fdout); close (fderr); if (pid != (pid_t)-1){ //printf ("kill %u\n",pid); kill (pid,SIGTERM); } } void SUBPROGRAM::send(PARAM_STRING gameid, PARAM_STRING line) { tosend.emplace_back(gameid,line); sendmore(); } /* Send more lines to the engine until we reach an empty line. This means we expect some answer from the engine before sending more. */ int SUBPROGRAM::sendmore() { int ret = 0; if (tosend.size() > 0 && (gameid.empty() || tosend[0].gameid == gameid)){ gameid = tosend[0].gameid; for (auto &l:tosend){ if (l.gameid != gameid){ break; }else{ ret++; write (fdin,l.line.c_str(),l.line.size()); } } tosend.erase(tosend.begin(),tosend.begin()+ret); nbsend += ret; } return ret; } void subprogram_exec (PARAM_STRING gameid, const char *cmd) { } void documentd_init_specs (DOC_UI_SPECS_receive &sp) { sp.width = sp.height = sp.content_width = sp.content_height = 1000; sp.mobile = false; sp.fontsize = 14; } static void documentd_chess_move(vector &programs, map &games, const char *gameid, PARAM_STRING move) { if (strlen(move.ptr)==4 && isalpha(move.ptr[0]) && isdigit(move.ptr[1]) && isalpha(move.ptr[2]) && isdigit(move.ptr[3])){ DOC_UI_SPECS_receive sp; documentd_init_specs (sp); vector res; unsigned col1 = move.ptr[0] - 'a'; unsigned line1 = 8-(move.ptr[1] - '0'); unsigned col2 = move.ptr[2] - 'a'; unsigned line2 = 8-(move.ptr[3] - '0'); string cmd1 = string_f("%u,%u,1",line1,col1); string cmd2 = string_f("%u,%u,1",line2,col2); vector steps; VARVAL_receive st; st.var = "place"; st.val = cmd1.c_str(); steps.emplace_back(st); st.val = cmd2.c_str(); steps.emplace_back(st); string msg; bool unknown; DOC_CONTEXT ctx; ctx.session = "session"; ctx.username = "user"; ctx.maywrite = true; documentd_playstep (programs,games,gameid,steps,ctx,sp,res,msg,unknown); } } static void documentd_chess_print (const char *user, map &games, const char *gameid) { DOC_UI_SPECS_receive sp; documentd_init_specs (sp); vector res; string msg; bool unknown; vector steps; VARVAL_receive st; st.var = "print"; st.val = "console"; steps.emplace_back(st); vector programs; DOC_CONTEXT ctx; ctx.session = "session"; ctx.username = "user"; ctx.maywrite = true; documentd_playstep (programs,games,gameid,steps,ctx,sp,res,msg,unknown); for (auto &r:res){ if (r.var == VAR_CONTENT){ printf("\n%s\n",r.val.c_str()); break; } } } /* Start all available game engines */ static void documentd_start_engines(vector &programs) { if (file_type("/usr/bin/stockfish")!=-1){ programs.emplace_back(CLASS_CHESS,"/usr/bin/stockfish"); auto &s = programs[programs.size()-1]; for (auto line:{"uci\n","ucinewgame\n","setoption name Skill Level value 0\n","isready\n"}){ s.send ("",line); } }else if (file_type("/usr/bin/gnuchess")!=-1){ programs.emplace_back(CLASS_CHESS,"/usr/bin/gnuchess -u"); auto &s = programs[programs.size()-1]; for (auto line:{"uci\n","","ucinewgame\n","isready\n"}){ s.send ("",line); } } } static map flags; /* Return the value of a flag. Return nullptr if the flag is not defined. */ const char *documentd_getflag(const char *flag) { const char *ret = nullptr; auto f = flags.find(flag); if (f != flags.end()){ ret = f->second.c_str(); } return ret; } /* Erase the last character (UTF8) of a string */ void documentd_eraselast (string &txt) { size_t size = txt.size(); if (size > 0){ unsigned pos = 0; while (1){ unsigned charsize = utf8_codepoint_size(txt[pos]); unsigned newpos = pos + charsize; if (newpos < size){ pos = newpos; }else{ break; } } txt.resize(pos); } } /* Copy some results from full to part without exceding REQ_CONTENT_CHUNK */ static void documentd_copy_chunk_res(vector &part, vector &full) { size_t size = 0; // No ++p in this loop as we remove the first entries until we break out the of loop // The erase method return the next p for (auto p=full.begin(); p != full.end(); ){ auto next_size = p->val.size() + size; if (next_size <= REQ_CONTENT_CHUNK){ part.emplace_back(move(*p)); p = full.erase(p); size = next_size; }else{ auto diff = REQ_CONTENT_CHUNK - size; if (diff > 0){ VARVAL tmp; tmp.var = p->var; tmp.val = p->val.substr(0,diff); p->val.erase(0,diff); part.emplace_back(move(tmp)); } break; } } } int main (int argc, char *argv[]) { glocal int ret = -1; glocal const char *control = "/var/run/documentd.sock"; glocal const char *clientsock = "/tmp/documentd_client.sock"; glocal const char *user = "bolixo"; glocal bool daemon = false; glocal const char *client_secretfile = "/etc/bolixo/secrets.client"; glocal const char *pidfile = "/var/run/documentd.pid"; glocal const char *hostname = NULL; glocal vector programs; translat_setlang ("eng,fr"); static const char *tbdic[]={"bolixo",NULL}; signal (SIGPIPE,SIG_IGN); glocal.ret = (argc,argv,tbdic); setproginfo ("documentd",VERSION,"Process document content"); setgrouparg ("Networking"); setarg ('c',"control","Unix socket for documentd-control",glocal.control,false); setarg ('C',"clientsock","Unix socket for documentd-client",glocal.clientsock,false); setgrouparg ("Misc."); setarg (' ',"user","Run the program as this user",glocal.user,false); setarg (' ',"daemon","Run in background",glocal.daemon,false); setarg (' ',"pidfile","File holding the PID of the process",glocal.pidfile,false); setarg (' ',"client-secrets","File holding client secrets for communication",glocal.client_secretfile,false); setarg (' ',"nodename",MSG_R(O_NODENAME),nodename,true); if (glocal.daemon){ syslog (LOG_ERR,"%s",msg); }else{ fprintf (stderr,"%s",msg); } if (glocal.daemon){ syslog (LOG_WARNING,"%s",msg); }else{ fprintf (stderr,"%s",msg); } // This is used for testing/debug //long long start = fdpass_getnow(); //long long end = fdpass_getnow(); //long long diff = end - start; //tlmp_warning ("set_para_spec %Ld.%06Ld",diff/1000000,diff%1000000); void wordproc_testparagraph (const char *line, unsigned width, unsigned fontsize, unsigned para_cursor); if (strcmp(argv[0],"testparagraph")==0){ if (argc > 1){ for (int i=1; i games; documentd_startgame(glocal.games,"boBOCHES",glocal.gameid,msg); // Trick to assign the users documentd_chess_print ("user1",glocal.games,glocal.gameid); documentd_chess_print ("robot",glocal.games,glocal.gameid); if (argc == 1){ documentd_start_engines(glocal.programs); }else{ string cmd = argv[1]; for (int i=2; i(); printf ("endclient %d\n",no); auto &s = glocal.programs[0]; if (no == 0){ // stdin documentd_chess_move(glocal.programs,glocal.games,glocal.gameid,line); documentd_chess_print("user1",glocal.games,glocal.gameid); }else if (s.is_fdout(no)){ const char *gameid = s.get_gameid(); if (gameid[0] == '\0'){ printf ("out: %s\n",line); }else{ printf ("out %s: %s\n",gameid,line); for (auto &m:glocal.games){ if (m.first == gameid){ string notify; bool done = false; m.second->engine_reply(line,notify,done); if (notify.size() > 0) documentd_chess_print("user1",glocal.games,glocal.gameid); if (done) s.reset_gameid(); s.sendmore(); break; } } } }else if (s.is_fderr(no)){ printf ("err: %s\n",line); } for (auto &s:glocal.programs){ o.inject (s.get_fdout(),nullptr); o.inject (s.get_fderr(),nullptr); } o.inject (0,nullptr); o.loop(); }else if (strcmp(argv[0],"testcalc")==0){ void calc_eval (int argc, char *argv[]); calc_eval (argc-1,argv+1); }else if (strcmp(argv[0],"doc2html")==0){ if (argc == 2){ string msg; FILE *fin = fopen (argv[1],"r"); if (fin == nullptr){ msg = "Can't open document"; }else{ char buf[100]; if (fgets(buf,sizeof(buf)-1,fin)!=NULL){ GAME_P p = documentd_newgame(buf,"id",msg); if (p != NULL){ DOC_READER doc(fin); p->load (doc,msg); if (msg.size() == 0){ DOC_CONTEXT ctx; DOC_UI_SPECS_receive sp; documentd_init_specs (sp); vector res; vector steps; VARVAL_receive rec; rec.var = REQ_PRINT; rec.val = "console"; steps.push_back(rec); vector unotifies; p->manyexec (steps,ctx,sp,res,unotifies); for (auto &r:res) printf ("var=%s: %s\n",r.var.c_str(),r.val.c_str()); } } } fclose (fin); } if (msg.size() > 0) tlmp_error ("Error: %s\n",msg.c_str()); } }else{ tlmp_error ("Invalid test command\n"); } return 0; int ret = -1; glocal map games; glocal unsigned messages_sent = 0; glocal string controlport = string_f("unix:%s",glocal.control); glocal string clientport = string_f("unix:%s",glocal.clientsock); glocal map client_secrets; glocal map> handles; // Handles used for playstep_more fdpass_readsecrets (glocal.client_secretfile,glocal.client_secrets); signal (SIGCHLD,SIG_IGN); documentd_start_engines(glocal.programs); (glocal.clientport,5); HANDLE_INFO *n = new HANDLE_INFO; info.data = n; // tlmp_error ("port=%s control=%s client=%s\n",info.port,glocal.controlport.c_str(),glocal.clientport.c_str()); if (string_cmp(info.port,glocal.controlport)==0){ n->type = TYPE_CONTROL; }else if (string_cmp(info.port,glocal.clientport)==0){ n->req.secret = fdpass_findsecret (glocal.client_secrets,info.port); n->type = TYPE_CLIENT; } // Is this client waiting for a notification ? bool found = false; for (auto g:glocal.games){ if (g.second->del_notification_fd(no)!=-1){ found = true; break; } } if (!found){ for (auto p=glocal.programs.begin(); p != glocal.programs.end(); p++){ if (p->is_fdout(no) || p->is_fderr(no)){ tlmp_error ("Engine %s ending",p->getclass()); glocal.programs.erase(p); break; } } } debug_printf (D_PROTO,"receive line: %s\n",line); HANDLE_INFO *c = (HANDLE_INFO*)info.data; static const char *tbtype[]={"none","control request","client request","subprogram"}; ERROR_PREFIX prefix ("%s: ",tbtype[c->type]); if (c->type == TYPE_CONTROL){ (this,c->req,line, info.linelen,endserver, endclient, no,c); vector tb; tb.push_back(string_f ("Version %s",VERSION)); instrument_status(tb); unsigned nbwait=0; for (auto g:glocal.games){ DATEASC act,mod; fdpass_asctime(g.second->get_last_activity(),act); if (g.second->is_modified()) fdpass_asctime(g.second->get_modified(),mod); tb.push_back(string_f("gameid: %s last_activity=%s modified=%s modified_by=%s revision=%u nbwait=%u sequence=%u" ,g.first.c_str() ,act.buf,mod.buf,g.second->get_modified_by(),g.second->get_revision() ,g.second->get_nbwait(),g.second->get_sequence())); nbwait += g.second->get_nbwait(); } tb.push_back(string_f("protocolstats: %d",protocol_stats)); tb.push_back(string_f("chessmaxskill: %u",chess_getmaxskill())); tb.push_back(string_f("bytes sent: requests=%u content=%zu script=%zu unotify=%zu notify=%zu" ,total_requests,total_content_size,total_script_size,total_unotify_size,total_notify_size)); tb.push_back(string_f("nbwaiting: %u",nbwait)); tb.push_back(string_f("handles: %zu",glocal.handles.size())); tb.push_back(string_f("subprograms: %zu",glocal.programs.size())); for (auto &p:glocal.programs){ tb.push_back(string_f("subprogram %s: nbsend=%u nbrec=%u gameid=%s command=%s" ,p.getclass(),p.getnbsend(),p.getnbrec(),p.getgameid(),p.getcommand())); } rep_status(tb); vector stats; for (auto g:glocal.games){ GAMESTAT st; st.gameid = g.first; st.modified = g.second->is_modified() ? g.second->get_modified() : 0; st.modified_by = g.second->get_modified_by(); st.last_activity = g.second->get_last_activity(); st.revision = g.second->get_revision(); stats.emplace_back(st); } rep_listgames(stats); toggle_instrument_file(on); protocol_stats = on; chess_setmaxskill(maxskill); // Save all games/documents in a file unsigned num=0; for (auto &g:glocal.games){ glocal GAME_P game = g.second; glocal const char *gameid = g.first.c_str(); (string_f("/tmp/game.%u",num),false); fprintf (fout,"%s\nmodified=%lu\nmodified_by=%s\nactivity=%lu\nboBO%s\n" ,glocal.gameid,glocal.game->get_modified() ,glocal.game->get_modified_by() ,glocal.game->get_last_activity() ,glocal.game->getclass()); DOC_WRITER doc(fout); glocal.game->save(doc,true); return 0; num++; } endserver = true; if (on){ debug_seton(); }else{ debug_setoff(); } debug_setfdebug (filename); // gamename gameid = success:b msg string msg; bool success = documentd_startgame(glocal.games,gamename,gameid,msg); rep_startgame(success,msg); // gameid = success:b msg string msg; bool success = documentd_endgame(glocal.games,gameid,"admin",revision,force,msg); rep_endgame (success,msg); glocal.games.clear(); // gameid = success:b msg bool success = false; string msg; auto g = glocal.games.find(gameid); if (g != glocal.games.end()){ success = true; g->second->resetgame(); }else{ msg = "Unknown game id"; } rep_resetgame (success,msg); // gameid steps:U{VARVAL}v width:u height:u = success:b msg res:U{VARVAL}v vector res; string msg; bool unknown; DOC_UI_SPECS_receive sp; sp.width = sp.height = 1000; sp.content_width = sp.content_height = 1000; sp.mobile = false; sp.fontsize = 14; DOC_CONTEXT ctx; ctx.session = "sessadmin"; ctx.username = "admin"; ctx.maywrite = true; bool success = documentd_playstep (glocal.programs,glocal.games,gameid,steps,ctx,sp,res,msg,unknown); rep_playstep(success,unknown,msg,res); // gameid = success:b msg bool success = false; string msg; auto g = glocal.games.find(gameid); if (g != glocal.games.end()){ glocal GAME_P game = g->second; success = true; (documentd_path(gameid),false); fprintf (fout,"boBO%s\n",glocal.game->getclass()); DOC_WRITER doc(fout); glocal.game->save(doc,true); return 0; }else{ msg = "Unknown game id"; } rep_save(success,msg); // gameid = success:b msg bool success = false; string msg; auto g = glocal.games.find(gameid); if (g != glocal.games.end()){ glocal.games.erase (g); } string tmp = documentd_path(gameid); FILE *fin = fopen (tmp.c_str(),"r"); if (fin == NULL){ msg = "Gameid file does not exist"; }else{ char buf[100]; if (fgets(buf,sizeof(buf)-1,fin)!=NULL){ GAME_P p = documentd_newgame(buf,gameid,msg); if (p != NULL){ glocal.games[gameid] = p; DOC_READER doc(fin); p->load (doc,msg); success = msg.size() == 0 ? true : false; } } fclose (fin); } rep_load(success,msg); flags[flag] = value; tlmp_error ("Invalid command: %s\n",line); endclient = true; }else if (c->type == TYPE_CLIENT){ (this,c->req,line,info.linelen, endserver, endclient,no,c); rep_test(true); // gamename gameid = success:b msg string msg; translat_selectlang(lang); bool success = documentd_startgame(glocal.games,gamename,gameid,msg); rep_startgame(success,msg); // gameid revision:u username force:b lang = success:b msg string msg; translat_selectlang(lang); bool success = documentd_endgame(glocal.games,gameid,username,revision,force,msg); rep_endgame (success,msg); vector res; string msg; translat_selectlang(lang); bool unknown; DOC_CONTEXT ctx; ctx.session = session; ctx.username = username; ctx.maywrite = maywrite; ctx.docnum = docnum; ctx.connectid = connectid; bool success = documentd_playstep (glocal.programs,glocal.games,gameid,steps,ctx,sp,res,msg,unknown); size_t size = 0; for (auto &r:res) size += r.val.size(); if (size <= REQ_CONTENT_CHUNK){ rep_playstep(success,unknown,msg,res,false,""); }else{ // playstep_more will be needed. vector part; documentd_copy_chunk_res(part,res); string handle = fs_makeid(); glocal.handles[handle] = move(res); rep_playstep(success,unknown,msg,part,true,handle); } auto h = glocal.handles.find(handle); if (h == glocal.handles.end()){ vector res; rep_playstep_more(false,"invalid handle",res,false); }else{ auto &hres = h->second; size_t size = 0; for (auto &r:hres) size += r.val.size(); if (size < REQ_CONTENT_CHUNK){ rep_playstep_more (true,"",hres,false); glocal.handles.erase(handle); }else{ vector res; documentd_copy_chunk_res(res,hres); rep_playstep_more (true,"",res,true); } } // Rename a document. It is possible that old_gameid is not a document, but a folder, so we must // rename all documents in this folder. string msg; translat_selectlang(lang); vector> to_renames; // Will contain the entries to rename // We do a first pass to identify all entries to rename // The second pass will complete the work. // We do this because glocal.games must not be changes during // the first pass. for (auto &g:glocal.games){ const char *pt; if (g.first == old_gameid){ // We have found a document with this name to_renames.emplace_back(old_gameid,new_gameid); break; }else if (is_start_any_of(g.first,pt,old_gameid) && pt[0] == '/'){ // old_gameid is in fact a directory. // We assume new_gameid is the new name of the directory. to_renames.emplace_back(g.first,string_f("%s%s",new_gameid,pt)); } } for (auto &r:to_renames){ // This is ok if the old document is missing auto oldg = glocal.games.find(r.first); auto newg = glocal.games.find(r.second); if (newg != glocal.games.end()){ msg = MSG_U(E_NEWEXIST,"Can't rename document, new name exist"); break; }else{ auto doc = oldg->second; const char *relname = r.second.c_str(); const char *pt; if (is_start_any_of(r.second,pt,"/projects/")) relname = pt; string msg = string_f(MSG_U(I_RENAMED,"

Attention

" "The document has been renamed by %s
" "Its new name is
" "

%s

" "Close the document and reopen it using the new name
" "All your modifications are preserved"),username,relname); documentd_create_popup(msg,doc); doc->setgameid(r.second); glocal.games.erase(r.first); glocal.games[r.second] = doc; } } if (msg.size() > 0){ rep_rename (false,msg); }else{ rep_rename (true,""); } string msg; BOB_TYPE content; auto g = glocal.games.find(gameid); if (g != glocal.games.end()){ GAME_P game = g->second; DOC_WRITER doc; doc.write (string_f("boBO%s\n",game->getclass())); game->save(doc,false); content = doc.getcontent(); game->resetmodified(); }else{ msg = "Unknown game id"; } if (msg.size() > 0){ content.clear(); rep_save(false,msg,content,false); }else{ rep_save (true,"",content,false); } BOB_TYPE content; rep_savemore (false,"Not inplemented",content,false); string msg; auto g = glocal.games.find(gameid); const char *type = (const char *)content.getbuffer(); GAME_P game = nullptr; if (g != glocal.games.end()){ game = g->second; }else{ game = documentd_newgame(type,gameid,msg); } if (game != nullptr){ glocal.games[gameid] = game; DOC_READER doc(type+9); game->load(doc,msg); } vector imbeds; if (game != nullptr && msg.size() > 0){ rep_load (false,msg,imbeds); }else{ auto sub = game->get_embed_list(); for (auto &s:sub){ if (glocal.games.find(s)==glocal.games.end()) imbeds.push_back(s); } rep_load (true,"",imbeds); } // gameid = success:b msg modified:b bool modified = false; const char *modified_by = ""; string msg; auto g = glocal.games.find(gameid); if (g != glocal.games.end()){ modified = g->second->is_modified(); modified_by = g->second->get_modified_by(); }else{ msg = "Unknown document"; } if (msg.size() > 0){ rep_is_modified(false,msg,false,"",0); }else{ rep_is_modified(true,"",modified,modified_by,g->second->get_revision()); } // connectid gameid username sequence:u = success:b msg script sequence:u auto g = glocal.games.find(gameid); if (g == glocal.games.end()){ rep_waitevent (false,"Invalid gameid","",0); }else{ GAME_P game = g->second; // This is an ASYNC. Fairly unique in Bolixo. // The connection is reserved for one waitevent call. // It means we can call rep_waitevent repeatedly // as well as documentd_client_rep_waitevent later. string rep; while (1){ const char *script = game->locate_event(sequence); if (script != nullptr){ //rep += script; total_notify_size += strlen(script); rep_waitevent (true,"",script,sequence); }else{ break; } } if (rep.size() > 0){ total_notify_size += rep.size(); rep_waitevent (true,"",rep,sequence); } { // tlmp_warning ("waitevent no=%d,%d username=%s connectid=%s",glocal.no,no,username,connectid); game->add_notification_fd(no,username,connectid); if (!game->waiting_user(username)){ string line; game->update_waiting_users(line); game->add_notification(line); } } } for (auto g:glocal.games) g.second->remove_session(session); rep_removesession(true,""); tlmp_error ("Invalid command: %s\n",line); endclient = true; }else if (c->type == TYPE_SUBPROGRAM){ for (auto &s:glocal.programs){ if (s.is_fdout(no)){ //printf ("out %s: %s\n",s.get_gameid(),line); s.inc_nbrec(); for (auto &m:glocal.games){ if (m.first == s.get_gameid()){ string notify; bool done = false; m.second->engine_reply(line,notify,done); if (done) s.reset_gameid(); s.sendmore(); m.second->add_notification(notify); break; } } }else if (s.is_fderr(no)){ tlmp_error ("engine %s error: %s\n",s.get_gameid(),line); } } } bool some_errors = false; if (fdpass_setcontrol(s,glocal.control,glocal.user)==-1){ some_errors = true; } // Register all subprograms for (auto &p:glocal.programs){ HANDLE_INFO *n = new HANDLE_INFO; n->type = TYPE_SUBPROGRAM; s.inject (p.get_fdout(),n); n = new HANDLE_INFO; n->type = TYPE_SUBPROGRAM; s.inject (p.get_fderr(),n); } if (!some_errors && s.is_ok()){ // Load saved games/documents if (file_type("/tmp/game.0")==0){ ("/tmp"); if (strncmp(basename,"game.",5)==0){ FILE *fin = fopen (path,"r"); if (fin == nullptr){ tlmp_error ("Can't open saved game %s (%s)\n",path,strerror(errno)); }else{ char buf1[1000],buf2[1000]; if (fgets(buf1,sizeof(buf1)-1,fin)!=nullptr && fgets(buf2,sizeof(buf2)-1,fin)!=nullptr){ strip_end (buf1); const char *pt; time_t mod = 0; if (is_start_any_of(buf2,pt,"modified=")){ mod = atoi(pt); fgets(buf2,sizeof(buf2)-1,fin); } string mod_by; if (is_start_any_of(buf2,pt,"modified_by=")){ mod_by = pt; strip_end (mod_by); fgets(buf2,sizeof(buf2)-1,fin); } time_t act = 0; if (is_start_any_of(buf2,pt,"activity=")){ act = atoi(pt); fgets(buf2,sizeof(buf2)-1,fin); } strip_end (buf2); string msg; if (!documentd_startgame(glocal.games,buf2,buf1,msg)){ tlmp_error ("Can't initialise game %s/%s (%s)\n",buf1,buf2,msg.c_str()); }else{ DOC_READER doc(fin); auto g = glocal.games[buf1]; g->load(doc,msg); g->setmodified(mod,mod_by.c_str()); g->setactivity(act); } } fclose (fin); } unlink (path); } } chmod (glocal.clientsock,0666); s.setrawmode(true); if (glocal.daemon){ daemon_init(glocal.pidfile,glocal.user); } (5); for (auto &g:glocal.games){ string line; g.second->update_waiting_users(line); if (line.size() > 0) g.second->add_notification(line); } netevent_loop(s,idle); ret = 0; } return ret; return glocal.ret; } #include #include FT_FREETYPE_H #define UTF8_ONE_BYTE_MASK 0b10000000 #define UTF8_ONE_BYTE_COUNT 0 #define UTF8_TWO_BYTE_MASK 0b11100000 #define UTF8_TWO_BYTE_COUNT 0b11000000 #define UTF8_THREE_BYTE_MASK 0b11110000 #define UTF8_THREE_BYTE_COUNT 0b11100000 #define UTF8_FOUR_BYTE_MASK 0b11111000 #define UTF8_FOUR_BYTE_COUNT 0b11110000 // This one could use a better name, I just don't know a better one (yet?) #define UTF8_OTHER_MASK 0b00111111 size_t utf8_codepoint_size(uint8_t text) { if((text & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_COUNT) { return 1; } if((text & UTF8_TWO_BYTE_MASK) == UTF8_TWO_BYTE_COUNT) { return 2; } if((text & UTF8_THREE_BYTE_MASK) == UTF8_THREE_BYTE_COUNT) { return 3; } return 4; } /* Compute the width and height of the string using a font library. */ unsigned documentd_displaylen (const char *title, unsigned fontsize, float size) { unsigned ret = 0; static bool some_errors = false; static bool is_init = false; static FT_Library library; static FT_Face face; /* handle to face object */ if (!is_init){ is_init = true; some_errors = true; int error = FT_Init_FreeType( &library ); if ( error ){ tlmp_error (" ... an error occurred during library initialization ...\n"); }else{ const char *fontfile = nullptr; for (auto s:{ "/usr/share/fonts/dejavu/DejaVuSerif.ttf", "/usr/share/fonts/liberation-sans/LiberationSans-Regular.ttf", "/usr/share/fonts/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/dejavu/DejaVuSans.ttf" }){ if (file_type(s)!=-1){ fontfile = s; break; } } if (fontfile == nullptr){ some_errors = true; tlmp_error ("tlmpweb_displaylen: No font file found"); }else{ error = FT_New_Face(library,fontfile,0,&face); if ( error == FT_Err_Unknown_File_Format ){ tlmp_error ("... the font file could be opened and read, but it appears\n" "... that its font format is unsupported\n"); }else if ( error ){ tlmp_error ("... another error code means that the font file could not\n" "... be opened or read, or that it is broken...\n"); }else{ some_errors = false; } } } } unsigned font_charsize = fontsize*64*size; //font_charsize = 13.6*64*size; if (!some_errors){ static unsigned last_charsize=0; //unsigned charsize = 13.6*64*size; if (font_charsize != last_charsize){ last_charsize = font_charsize; int error = FT_Set_Char_Size( face, /* handle to face object */ 0, /* char_width in 1/64th of points */ font_charsize, /* char_height in 1/64th of points */ 0, /* horizontal device resolution */ 0 ); /* vertical device resolution */ if (error){ tlmp_error ("Set_Char_Size %u error\n",font_charsize); }else{ some_errors = false; } } } if (some_errors){ ret = strlen(title)*9; }else{ const char *pt = title; while (*pt != '\0'){ unsigned t = *pt++; size_t charsize = utf8_codepoint_size(t); switch(charsize){ case 1: break; case 2: if (*pt == '\0') break; t = (t<<8)+*pt++; break; case 3: break; case 4: break; } struct CACHECHAR{ unsigned car; unsigned font_charsize; CACHECHAR(unsigned _car, unsigned _font_charsize){ car = _car; font_charsize = _font_charsize; } bool operator < (const CACHECHAR &n) const { return tie(font_charsize,car) < tie(n.font_charsize,n.car); } }; static map cache; auto &cached_width = cache[CACHECHAR(t,font_charsize)]; if (cached_width == 0){ unsigned glyph_index = FT_Get_Char_Index( face, t ); /* load glyph image into the slot (erase previous one) */ int error = FT_Load_Glyph( face, glyph_index, FT_LOAD_DEFAULT ); if ( error ) continue; /* ignore errors */ /* convert to an anti-aliased bitmap */ error = FT_Render_Glyph( face->glyph, FT_RENDER_MODE_NORMAL ); if ( error ) continue; //tlmp_error ("dislay_len: %c -> %u\n",t,(unsigned)(face->glyph->advance.x >> 6)); cached_width = face->glyph->advance.x >> 6; //ret += 1; // One pixel between characters. advance takes care of that // but for now, we are not using the same font as the browser } ret += cached_width; } } return ret; } void _F_button_bar::status(const char *gameid, string &lines) { } // Draw the button bar void button_bar (_F_button_bar &c, GAME *game, bool mobile, bool maywrite, string &lines) { if (maywrite){ const char *gameid = game->get_gameid(); string but_lines; DOC_BUTTON_SPECS specs(mobile); documentd_button_start(but_lines,gameid); c.draw (specs,gameid,but_lines); documentd_button_end(but_lines); string st_lines; c.status (gameid,st_lines); if (st_lines.empty()){ lines += "

\n"; lines += move(but_lines); }else{ lines += "
\n"; lines += "
\n"; lines += move(but_lines); lines += "
\n"; lines += "
\n"; lines += move(st_lines); lines += "
\n"; } }else{ lines += "
\n"; lines += MSG_U(M_READONLY,"Read only"); } lines += "
\n"; } void _F_doc_layout::menu_bar(class DOC_BUTTON_SPECS &specs, const char *gameid, std::string &lines) { } void _F_doc_layout::menu_status(const char *gameid, std::string &lines) { } std::string _F_doc_layout::hscroll(const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, const char *gameid, unsigned board_width, unsigned board_height, unsigned scroll_thick) { return ""s; } std::string _F_doc_layout::vscroll(const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, const char *gameid, unsigned board_width, unsigned board_height, unsigned scroll_thick) { return ""s; } /* Manage the layout of a document or game */ void doc_layout ( _F_doc_layout &c, const char *id_prefix, GAME *game, const DOC_CONTEXT &ctx, const DOC_UI_SPECS_receive &sp, const char *doc_suffix, bool scroll, bool is_full, // Document full browser tab mode VARVAL &v) { unsigned waiting_users_width = sp.mobile ? 62 : 42; const unsigned scroll_thick = scroll ? (sp.mobile ? 12 : 6) : 0; unsigned board_width; // Space for user list and scroll unsigned board_height; // Some document requires a square area. doc_layout must received this square boolean. // So far no document using doc_layout requires this. bool square = false; if (!square){ board_width = sp.content_width - waiting_users_width - scroll_thick; // Space for user list and scroll board_height = sp.content_height-scroll_thick; }else{ board_width = sp.content_width - waiting_users_width; // Space for user list if (sp.content_height < board_width) board_width = sp.content_height; board_width -= scroll_thick; board_height = board_width; } v.var = VAR_CONTENT; v.val += "\n"; v.val += "\n"; glocal c; glocal is_full; (game,sp.mobile,ctx.maywrite,v.val); if (glocal.is_full){ documentd_bar_button (lines,BUTTON_RELOAD,documentd_menu_get_reload(specs),specs,false ,MSG_U(I_RELOADPAGE,"Reload browser page")); } glocal.c.menu_bar(specs,gameid,lines); glocal.c.menu_status(gameid,lines); const char *gameid = game->get_gameid(); v.val += string_f("
\n",id_prefix,doc_suffix); string script,onevent; string content = c.content (ctx,sp,gameid,script,onevent,board_width,board_height,scroll_thick); v.val += string_f("
\n",gameid,board_width,board_height,onevent.c_str()); v.val += move(content); if (scroll){ // For now, caller gets minimal scroll bar and have no control over its look v.val += "
\n"; v.val += string_f("\n",board_width,scroll_thick); v.val += c.hscroll(ctx,sp,gameid,board_width,board_height,scroll_thick); v.val += "\n"; v.val += "
\n"; v.val += "
\n"; v.val += string_f("
",scroll_thick,board_height); v.val += "\n"; v.val += c.vscroll(ctx,sp,gameid,board_width,board_height,scroll_thick); v.val += "\n"; v.val += "
\n"; }else{ v.val += "
\n"; } if (script.size() > 0){ v.val += "\n"; } game->draw_waiting_users(v.val,waiting_users_width,board_height,"flex:0 0 auto;"); v.val += "
\n"; // Status line v.val += "
\n"; v.val += string_f("
 
\n",gameid); documentd_chat (v.val,ctx.username,sp.mobile,game->chat,sp.width-20,sp.mobile ? 200 : 100); v.val += "
\n"; } /* Request a refresh of the web page */ void documentd_action_reload(vector &res) { VARVAL var; var.var = VAR_SCRIPT; var.val = "callrefresh();\n"; res.emplace_back(move(var)); }