mirror of
				https://github.com/zenorogue/hyperrogue.git
				synced 2025-10-31 14:02:59 +00:00 
			
		
		
		
	improved presentation support in RogueViz
This commit is contained in:
		
							
								
								
									
										383
									
								
								rogueviz/presentation.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								rogueviz/presentation.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,383 @@ | ||||
| #ifndef PRESENTATION_CPP | ||||
| #define PRESENTATION_CPP | ||||
|  | ||||
| #include "../rogueviz/rogueviz.h" | ||||
|  | ||||
| namespace rogueviz { | ||||
|  | ||||
| #if CAP_RVSLIDES | ||||
| namespace pres { | ||||
|  | ||||
| /* maks graphs in presentations */ | ||||
| struct grapher { | ||||
|  | ||||
|   ld minx, miny, maxx, maxy; | ||||
|    | ||||
|   shiftmatrix T; | ||||
|    | ||||
|   grapher(ld _minx, ld _miny, ld _maxx, ld _maxy) : minx(_minx), miny(_miny), maxx(_maxx), maxy(_maxy) { | ||||
|     auto& cd = *current_display; | ||||
|      | ||||
|     ld xpixels = 2 * min(cd.xcenter - cd.xmin, cd.xmax - cd.xcenter); | ||||
|     ld ypixels = 2 * min(cd.ycenter - cd.ymin, cd.ymax - cd.ycenter); | ||||
|      | ||||
|     ld sca = min(abs(xpixels / (maxx-minx)), abs(ypixels / (maxy-miny))); | ||||
|      | ||||
|     ld medx = (minx + maxx) / 2; | ||||
|     ld medy = (miny + maxy) / 2; | ||||
|  | ||||
|     hyperpoint zero = atscreenpos(cd.xcenter - sca * medx, cd.ycenter + sca * medy, 1) * C0; | ||||
|  | ||||
|     hyperpoint zero10 = atscreenpos(cd.xcenter - sca * medx + sca, cd.ycenter + sca * medy, 1) * C0; | ||||
|     hyperpoint zero01 = atscreenpos(cd.xcenter - sca * medx, cd.ycenter + sca * medy - sca, 1) * C0; | ||||
|      | ||||
|     T = shiftless(Id); | ||||
|     T.T[LDIM] = zero; | ||||
|     T.T[0] = zero10 - zero; | ||||
|     T.T[1] = zero01 - zero; | ||||
|      | ||||
|     T.T = transpose(T.T); | ||||
|     } | ||||
|  | ||||
|   void line(hyperpoint h1, hyperpoint h2, color_t col) { | ||||
|     curvepoint(h1); | ||||
|     curvepoint(h2); | ||||
|     queuecurve(T, col, 0, PPR::LINE).flags |= POLY_FORCEWIDE; | ||||
|     } | ||||
|    | ||||
|   void arrow(hyperpoint h1, hyperpoint h2, ld sca) { | ||||
|     line(h1, h2, 0xFF); | ||||
|     hyperpoint h = h2 - h1; | ||||
|     ld siz = hypot_d(2, h); | ||||
|     h *= sca / siz; | ||||
|     curvepoint(h2); | ||||
|     curvepoint(h2 - spin(15*degree) * h); | ||||
|     curvepoint(h2 - spin(-15*degree) * h); | ||||
|     curvepoint(h2); | ||||
|     queuecurve(T, 0xFF, 0xFF, PPR::LINE); | ||||
|     } | ||||
|    | ||||
|   shiftmatrix pos(ld x, ld y, ld sca) { | ||||
|     transmatrix P = Id; | ||||
|     P[0][0] = sca; | ||||
|     P[1][1] = sca; | ||||
|     P[0][LDIM] = x; | ||||
|     P[1][LDIM] = y; | ||||
|     return T * P; | ||||
|     } | ||||
|    | ||||
|   }; | ||||
|  | ||||
| hyperpoint p2(ld x, ld y) { return LDIM == 2 ? point3(x, y, 1) : point31(x, y, 0); } | ||||
|  | ||||
| /* temporary hooks */ | ||||
|  | ||||
| using namespace hr::tour; | ||||
|  | ||||
| template<class T, class U> void add_temporary_hook(int mode, hookset<T>& m, int prio, U&& hook) { | ||||
|   if(mode == pmStart) { | ||||
|     int p = addHook(m, prio, hook); | ||||
|     on_restore([&m, p] {  | ||||
|       delHook(m, p);  | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| void add_stat(presmode mode, const bool_reaction_t& stat) { | ||||
|   add_temporary_hook(mode, hooks_prestats, 200, stat); | ||||
|   } | ||||
|  | ||||
| void no_other_hud(presmode mode) { | ||||
|   add_temporary_hook(mode, hooks_prestats, 300, [] { return true; }); | ||||
|   } | ||||
|  | ||||
| void empty_screen(presmode mode, color_t col = 0xFFFFFFFF) { | ||||
|   if(mode == pmStart) { | ||||
|     tour::slide_backup(nomap, true); | ||||
|     tour::slide_backup(backcolor, col); | ||||
|     tour::slide_backup(ringcolor, color_t(0)); | ||||
|     tour::slide_backup<color_t>(dialog::dialogcolor, 0); | ||||
|     tour::slide_backup<color_t>(forecolor, 0); | ||||
|     tour::slide_backup<color_t>(bordcolor, 0xFFFFFFFF); | ||||
|     tour::slide_backup(vid.aurastr, 0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| map<string, texture::texture_data> textures; | ||||
|  | ||||
| void draw_texture(texture::texture_data& tex) { | ||||
|   static vector<glhr::textured_vertex> rtver(4); | ||||
|  | ||||
|   ld tx = tex.tx; | ||||
|   ld ty = tex.ty; | ||||
|   ld os = max(tx, ty); | ||||
|   ld scalex = (vid.xres/2 - 2 * vid.fsize) / (current_display->radius * tx / os); | ||||
|   ld scaley = (vid.yres/2 - 2 * vid.fsize) / (current_display->radius * ty / os); | ||||
|   ld scale = min(scalex, scaley); | ||||
|   scale *= 2; | ||||
|  | ||||
|   for(int i=0; i<4; i++) { | ||||
|     ld cx[4] = {1,0,0,1}; | ||||
|     ld cy[4] = {1,1,0,0}; | ||||
|     rtver[i].texture[0] = (tex.base_x + (cx[i] ? tex.strx : 0.)) / tex.twidth; | ||||
|     rtver[i].texture[1] = (tex.base_y + (cy[i] ? tex.stry : 0.)) / tex.twidth; | ||||
|     rtver[i].coords[0] = (cx[i]*2-1) * scale * (tx / tex.twidth); | ||||
|     rtver[i].coords[1] = (cy[i]*2-1) * scale * (ty / tex.theight); | ||||
|     rtver[i].coords[2] = 1; | ||||
|     rtver[i].coords[3] = 1; | ||||
|     } | ||||
|    | ||||
|   println(hlog, tie(tex.tx, tex.base_x, tex.strx, tex.twidth)); | ||||
|   println(hlog, tie(tex.ty, tex.base_y, tex.stry, tex.theight)); | ||||
|   println(hlog, tie(tx, ty, os, scalex, scaley, scale)); | ||||
|    | ||||
|   glhr::be_textured(); | ||||
|   current_display->set_projection(0, false); | ||||
|   glBindTexture(GL_TEXTURE_2D, tex.textureid); | ||||
|   glhr::color2(0xFFFFFFFF); | ||||
|   glhr::id_modelview(); | ||||
|   current_display->set_mask(0); | ||||
|   glhr::prepare(rtver); | ||||
|   glhr::set_depthtest(false); | ||||
|   glDrawArrays(GL_TRIANGLE_FAN, 0, 4); | ||||
|   } | ||||
|    | ||||
| void show_picture(presmode mode, string s) { | ||||
|   if(mode == pmStartAll) { | ||||
|     auto& tex = textures[s]; | ||||
|     println(hlog, "rt = ", tex.readtexture(s)); | ||||
|     println(hlog, "gl = ", tex.loadTextureGL()); | ||||
|     } | ||||
|   add_stat(mode, [s] { | ||||
|     auto& tex = textures[s]; | ||||
|     flat_model_enabler fme;     | ||||
|     draw_texture(tex); | ||||
|     return false; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| int video_start = 0; | ||||
|  | ||||
| void read_all(int fd, void *buf, int cnt) { | ||||
|   char *cbuf = (char*) buf; | ||||
|   while(cnt > 0) { | ||||
|     int qt = read(fd, cbuf, cnt); | ||||
|     if(qt <= 0) break; | ||||
|     cbuf += qt; | ||||
|     cnt -= qt; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| /* note: this loads the whole animation uncompressed into memory, so it is suitable only for short presentations */ | ||||
| void show_animation(presmode mode, string s, int sx, int sy, int frames, int fps) { | ||||
|   if(mode == pmStartAll) { | ||||
|     array<int, 2> tab; | ||||
|     if(pipe(&tab[0])) { | ||||
|       addMessage(format("Error: %s", strerror(errno))); | ||||
|       return; | ||||
|       } | ||||
|  | ||||
|     int pid = fork(); | ||||
|     fflush(stdout); | ||||
|  | ||||
|     fprintf(stderr, "pipe is %d:%d\n", tab[0], tab[1]); | ||||
|  | ||||
|     if(pid == 0) { | ||||
|       fprintf(stderr, "in child\n"); | ||||
|      fprintf(stderr, "making fformat\n"); | ||||
|       string fformat = "ffmpeg -y -i " + s + " -f rawvideo -pix_fmt bgra /dev/fd/" + its(tab[1]);     | ||||
|       int sys = system(fformat.c_str()); | ||||
|       ::close(tab[0]); | ||||
|       fprintf(stderr, "system call returned %d: %s\n", sys, strerror(errno)); | ||||
|       ::close(tab[1]); | ||||
|       exit(0); | ||||
|       } | ||||
|        | ||||
|     ::close(tab[1]); | ||||
|     for(int i=0; i<frames; i++) { | ||||
|       auto& tex = textures[s + "@" + its(i)]; | ||||
|       tex.strx = tex.tx = sx; | ||||
|       tex.stry = tex.ty = sy; | ||||
|       tex.twidth = next_p2(tex.tx); | ||||
|       tex.theight = next_p2(tex.ty); | ||||
|       tex.base_x = tex.base_y = 0; | ||||
|       tex.texture_pixels.resize(tex.twidth * tex.theight); | ||||
|        | ||||
|       for(int y=0; y<sy; y++) { | ||||
|         read_all(tab[0], &tex.texture_pixels[tex.twidth * y], 4 * sx); | ||||
|         } | ||||
|        | ||||
|       println(hlog, "load frame ", i, " = ", tex.loadTextureGL(), " color = ", tex.texture_pixels[0]); | ||||
|       // tex.loadTextureGL(); | ||||
|       } | ||||
|      | ||||
|     ::close(tab[0]); | ||||
|     println(hlog, "waiting"); | ||||
|     wait(nullptr); | ||||
|     println(hlog, "waited"); | ||||
|     video_start = ticks; | ||||
|     } | ||||
|   add_stat(mode, [s, frames, fps] { | ||||
|     int f = (ticks - video_start) / 1000. * fps; | ||||
|     f %= frames; | ||||
|     auto& tex = textures[s + "@" + its(f)]; | ||||
|     flat_model_enabler fme;     | ||||
|     draw_texture(tex); | ||||
|     return false; | ||||
|     }); | ||||
|    | ||||
|   } | ||||
|  | ||||
| void choose_presentation() { | ||||
|   gamescreen(2); | ||||
|  | ||||
|   getcstat = ' '; | ||||
|    | ||||
|   dialog::init(XLAT("presentations"), 0xFFD500); | ||||
|  | ||||
|   ss::for_all_slideshows([] (string title, slide *sl, char ch) { | ||||
|     dialog::addItem(title, ch); | ||||
|     dialog::add_action([sl] {  | ||||
|       tour::slides = sl; | ||||
|       if(!tour::texts) nomenukey = true; | ||||
|       popScreenAll(); | ||||
|       tour::start(); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|   dialog::addBreak(100); | ||||
|    | ||||
|   dialog::addBoolItem_action(XLAT("enable/disable texts"), tour::texts, '7'); | ||||
|    | ||||
|   dialog::display(); | ||||
|   } | ||||
|  | ||||
| int phooks =  | ||||
|   0 | ||||
|   + addHook(dialog::hooks_display_dialog, 100, [] () { | ||||
|     if(current_screen_cfunction() == showStartMenu) {  | ||||
|       dialog::addBreak(100); | ||||
|       dialog::addBigItem(XLAT("presentations"), 'p'); | ||||
|       dialog::add_action([] () { pushScreen(choose_presentation); }); | ||||
|       dialog::addInfo(XLAT("presentation")); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
| static ld angle = 0; | ||||
| static int dir = -1; | ||||
|  | ||||
| void use_angledir(presmode mode, bool reset) { | ||||
|   if(mode == pmStart && reset) | ||||
|     angle = 0, dir = -1; | ||||
|   add_temporary_hook(mode, shmup::hooks_turn, 200, [] (int i) { | ||||
|     angle += dir * i / 500.; | ||||
|     if(angle > M_PI/2) angle = M_PI/2; | ||||
|     if(angle < 0) angle = 0; | ||||
|     return false; | ||||
|     }); | ||||
|    | ||||
|   if(mode == pmKey) dir = -dir; | ||||
|   } | ||||
|  | ||||
| void compare_projections(presmode mode, eModel a, eModel b) { | ||||
|   static function<void()> w; | ||||
|   if(mode == pmStart) { | ||||
|     w = wrap_drawfullmap; | ||||
|     tour::slide_backup(wrap_drawfullmap, w); | ||||
|     wrap_drawfullmap = [a, b] { | ||||
|       if(1) { | ||||
|         dynamicval<ld> xmin(current_display->xmin, 0); | ||||
|         dynamicval<ld> xmax(current_display->xmax, 0.49); | ||||
|         dynamicval<eModel> pm(pmodel, a); | ||||
|         calcparam(); | ||||
|         w(); | ||||
|         current_display->xmin = .51; | ||||
|         current_display->xmax = 1; | ||||
|         pmodel = b; | ||||
|         calcparam(); | ||||
|         w(); | ||||
|         } | ||||
|       calcparam(); | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| /* default RogueViz tour */ | ||||
|  | ||||
| vector<slide> rvslides; | ||||
| extern vector<slide> rvslides_default; | ||||
|  | ||||
| slide *gen_rvtour() { | ||||
|   rvslides = rvslides_default; | ||||
|   callhooks(hooks_build_rvtour, rvslides); | ||||
|   rvslides.emplace_back( | ||||
|     slide{"THE END", 99, LEGAL::ANY | FINALSLIDE, | ||||
|     "Press '5' to leave the presentation.", | ||||
|     [] (presmode mode) { | ||||
|       firstland = specialland = laIce; | ||||
|       if(mode == 4) restart_game(rg::tour); | ||||
|       } | ||||
|     }); | ||||
|   return &rvslides[0]; | ||||
|   } | ||||
|  | ||||
| vector<slide> rvslides_default = { | ||||
|     {"RogueViz", 999, LEGAL::ANY,  | ||||
|       "This is a presentation of RogueViz, which " | ||||
|       "is an adaptation of HyperRogue as a visualization tool " | ||||
|       "rather than a game. Hyperbolic space is great " | ||||
|       "for visualizing some kinds of data because of the vast amount " | ||||
|       "of space.\n\n" | ||||
|       "Press '5' to switch to the standard HyperRogue tutorial. " | ||||
|       "Press ESC to look at other functions of this presentation." | ||||
|       , | ||||
|       [] (presmode mode) { | ||||
|         slidecommand = "the standard presentation"; | ||||
|         if(mode == pmStartAll) firstland = specialland = laPalace; | ||||
|         if(mode == 4) { | ||||
|           tour::slides = default_slides; | ||||
|           while(tour::on) restart_game(rg::tour); | ||||
|           firstland = specialland = laIce; | ||||
|           tour::start(); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|     {"straight lines in the Palace", 999, LEGAL::ANY,  | ||||
|       "One simple slide about HyperRogue. Press '5' to show some hyperbolic straight lines.", | ||||
|       [] (presmode mode) { | ||||
|        using namespace linepatterns; | ||||
|        slidecommand = "toggle the Palace lines"; | ||||
|        if(mode == 4) patPalace.color = 0xFFD500FF; | ||||
|        if(mode == 3) patPalace.color = 0xFFD50000; | ||||
|         } | ||||
|       }, | ||||
|   }; | ||||
|  | ||||
| int rvtour_hooks =  | ||||
|   addHook(hooks_slide, 100, [] (int mode) { | ||||
|     if(currentslide == 0 && slides == default_slides) { | ||||
|       slidecommand = "RogueViz presentation"; | ||||
|       if(mode == 1) | ||||
|         help +=  | ||||
|           "\n\nYour version of HyperRogue is compiled with RogueViz. " | ||||
|           "Press '5' to switch to the RogueViz slides. Watching the " | ||||
|           "common HyperRogue tutorial first is useful too, " | ||||
|           "as an introduction to hyperbolic geometry.";          | ||||
|       if(mode == 4) { | ||||
|         slides = gen_rvtour(); | ||||
|         while(tour::on) restart_game(rg::tour); | ||||
|         tour::start(); | ||||
|         } | ||||
|       } | ||||
|     }) + | ||||
|   addHook(tour::ss::hooks_extra_slideshows, 100, [] (tour::ss::slideshow_callback cb) { | ||||
|     if(rogueviz::pres::rvslides.empty()) pres::gen_rvtour(); | ||||
|     cb(XLAT("RogueViz mixed bag"), &pres::rvslides[0], 'r'); | ||||
|     }) + | ||||
|   0; | ||||
|  | ||||
| } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| #endif | ||||
| @@ -26,3 +26,4 @@ | ||||
| #include "sumotron.cpp" | ||||
| #include "noniso-honeycombs.cpp" | ||||
| #include "random-walk.cpp" | ||||
| #include "presentation.cpp" | ||||
|   | ||||
| @@ -971,11 +971,6 @@ int readArgs() { | ||||
|   else if(argis("-lq")) { | ||||
|     shift_arg_formula(linequality); | ||||
|     } | ||||
| #if CAP_RVSLIDES | ||||
|   else if(argis("-rvpres")) { | ||||
|     tour::slides = rvtour::gen_rvtour(); | ||||
|     } | ||||
| #endif | ||||
|   else if(argis("-nolegend")) { | ||||
|     legend.clear(); | ||||
|     } | ||||
| @@ -1132,82 +1127,6 @@ void showMenu() { | ||||
|   dialog::display(); | ||||
|   } | ||||
|  | ||||
| #if CAP_RVSLIDES | ||||
| namespace rvtour { | ||||
|  | ||||
| using namespace tour; | ||||
|  | ||||
| vector<slide> rvslides; | ||||
| extern vector<slide> rvslides_default; | ||||
|  | ||||
| slide *gen_rvtour() { | ||||
|   rvslides = rvslides_default; | ||||
|   callhooks(hooks_build_rvtour, rvslides); | ||||
|   rvslides.emplace_back( | ||||
|     slide{"THE END", 99, LEGAL::ANY | FINALSLIDE, | ||||
|     "Press '5' to leave the presentation.", | ||||
|     [] (presmode mode) { | ||||
|       firstland = specialland = laIce; | ||||
|       if(mode == 4) restart_game(rg::tour); | ||||
|       } | ||||
|     }); | ||||
|   return &rvslides[0]; | ||||
|   } | ||||
|  | ||||
| vector<slide> rvslides_default = { | ||||
|     {"RogueViz", 999, LEGAL::ANY,  | ||||
|       "This is a presentation of RogueViz, which " | ||||
|       "is an adaptation of HyperRogue as a visualization tool " | ||||
|       "rather than a game. Hyperbolic space is great " | ||||
|       "for visualizing some kinds of data because of the vast amount " | ||||
|       "of space.\n\n" | ||||
|       "Press '5' to switch to the standard HyperRogue tutorial. " | ||||
|       "Press ESC to look at other functions of this presentation." | ||||
|       , | ||||
|       [] (presmode mode) { | ||||
|         slidecommand = "the standard presentation"; | ||||
|         if(mode == pmStartAll) firstland = specialland = laPalace; | ||||
|         if(mode == 4) { | ||||
|           tour::slides = default_slides; | ||||
|           while(tour::on) restart_game(rg::tour); | ||||
|           firstland = specialland = laIce; | ||||
|           tour::start(); | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|     {"straight lines in the Palace", 999, LEGAL::ANY,  | ||||
|       "One simple slide about HyperRogue. Press '5' to show some hyperbolic straight lines.", | ||||
|       [] (presmode mode) { | ||||
|        using namespace linepatterns; | ||||
|        slidecommand = "toggle the Palace lines"; | ||||
|        if(mode == 4) patPalace.color = 0xFFD500FF; | ||||
|        if(mode == 3) patPalace.color = 0xFFD50000; | ||||
|         } | ||||
|       }, | ||||
|   }; | ||||
|  | ||||
| int rvtour_hooks =  | ||||
|   addHook(hooks_slide, 100, [] (int mode) { | ||||
|     if(currentslide == 0 && slides == default_slides) { | ||||
|       slidecommand = "RogueViz presentation"; | ||||
|       if(mode == 1) | ||||
|         help +=  | ||||
|           "\n\nYour version of HyperRogue is compiled with RogueViz. " | ||||
|           "Press '5' to switch to the RogueViz slides. Watching the " | ||||
|           "common HyperRogue tutorial first is useful too, " | ||||
|           "as an introduction to hyperbolic geometry.";          | ||||
|       if(mode == 4) { | ||||
|         slides = gen_rvtour(); | ||||
|         while(tour::on) restart_game(rg::tour); | ||||
|         tour::start(); | ||||
|         } | ||||
|       } | ||||
|     }) + | ||||
|   0; | ||||
|  | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool default_help() { | ||||
|   if(!vizid) return false; | ||||
|  | ||||
| @@ -1234,31 +1153,11 @@ auto hooks  = | ||||
|   addHook(shmup::hooks_kill, 100, activate) + | ||||
|   addHook(hooks_o_key, 100, o_key) + | ||||
|    | ||||
| #if CAP_RVSLIDES | ||||
|   addHook(tour::ss::hooks_extra_slideshows, 100, [] (tour::ss::slideshow_callback cb) { | ||||
|     if(rogueviz::rvtour::rvslides.empty()) rvtour::gen_rvtour(); | ||||
|     cb(XLAT("RogueViz mixed bag"), &rvtour::rvslides[0], 'r'); | ||||
|     }) + | ||||
| #endif | ||||
|    | ||||
|   addHook(dialog::hooks_display_dialog, 100, [] () { | ||||
|     if(current_screen_cfunction() == showMainMenu) { | ||||
|       dialog::addItem(XLAT("rogueviz menu"), 'u');  | ||||
|       dialog::add_action_push(rogueviz::showMenu); | ||||
|       } | ||||
|     #if CAP_RVSLIDES | ||||
|     if(current_screen_cfunction() == showStartMenu) { | ||||
|       dialog::addBreak(100); | ||||
|       dialog::addBigItem(XLAT("RogueViz"), 'r'); | ||||
|       dialog::add_action([] () {         | ||||
|         tour::slides = rogueviz::rvtour::gen_rvtour(); | ||||
|         popScreenAll(); | ||||
|         tour::start(); | ||||
|         printf("tour start\n"); | ||||
|         }); | ||||
|       dialog::addInfo(XLAT("see the visualizations")); | ||||
|       } | ||||
|     #endif | ||||
|     }) + | ||||
|   addHook(hooks_welcome_message, 100, [] () { | ||||
|     if(vizid) addMessage(XLAT("Welcome to RogueViz!")); | ||||
|   | ||||
| @@ -127,7 +127,7 @@ namespace rogueviz { | ||||
|   void close(); | ||||
|   extern bool showlabels; | ||||
|    | ||||
|   namespace rvtour { | ||||
|   namespace pres { | ||||
|     using namespace hr::tour; | ||||
|     inline hookset<void(vector<slide>&)> hooks_build_rvtour; | ||||
|     slide *gen_rvtour(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Zeno Rogue
					Zeno Rogue