1
0
mirror of https://github.com/zenorogue/hyperrogue.git synced 2024-11-23 21:07:17 +00:00
hyperrogue/rogueviz/flocking.cpp

662 lines
20 KiB
C++
Raw Normal View History

2018-11-17 19:25:47 +00:00
// flocking simulations
// Copyright (C) 2018 Zeno and Tehora Rogue, see 'hyper.cpp' for details
// based on Flocking by Daniel Shiffman (which in turn implements Boids by Craig Reynold)
// https://processing.org/examples/flocking.html
// Our implementation simplifies some equations a bit.
2018-11-17 19:25:47 +00:00
// example parameters:
// flocking on a torus:
2020-03-20 18:48:21 +00:00
// -t2 3 0 0 3 0 -geo 1 -flocking 10 -rvshape 3
2018-11-17 19:25:47 +00:00
// flocking on the Zebra quotient:
// -geo 4 -flocking 10 -rvshape 3 -zoom .9
// press 'o' when flocking active to change the parameters.
2018-11-17 19:25:47 +00:00
#include "rogueviz.h"
2018-11-17 19:25:47 +00:00
namespace rogueviz {
namespace flocking {
2021-03-30 22:23:01 +00:00
void init();
2018-11-17 19:25:47 +00:00
int N;
bool draw_lines = false, draw_tails = false;
2018-11-17 19:25:47 +00:00
2018-11-19 17:59:22 +00:00
int follow = 0;
string follow_names[3] = {"nothing", "specific boid", "center of mass"};
ld follow_dist = 0;
2018-11-17 19:25:47 +00:00
map<cell*, map<cell*, transmatrix>> relmatrices;
ld ini_speed = .5;
ld max_speed = 1;
ld sep_factor = 1.5;
ld sep_range = .25;
ld align_factor = 1;
ld align_range = .5;
ld coh_factor = 1;
ld coh_range = 2.5;
2018-11-17 19:25:47 +00:00
ld check_range = 2.5;
bool swarm;
char shape = 'b';
2020-07-29 21:34:00 +00:00
vector<tuple<shiftpoint, shiftpoint, color_t> > lines;
2018-11-17 19:25:47 +00:00
// parameters of each boid
// m->base: the cell it is currently on
// m->vel: velocity
// m->at: determines the position and speed:
// m->at * (0, 0, 1) is the current position (in Minkowski hyperboloid coordinates relative to m->base)
// m->at * (m->vel, 0, 0) is the current velocity vector (tangent to the Minkowski hyperboloid)
// m->pat: like m->at but relative to the screen
int precision = 10;
2018-11-17 19:25:47 +00:00
void simulate(int delta) {
int iter = 0;
while(delta > precision && iter < (swarm ? 10000 : 100)) {
simulate(precision); delta -= precision;
iter++;
}
2018-11-17 19:25:47 +00:00
ld d = delta / 1000.;
int N = isize(vdata);
vector<transmatrix> pats(N);
2020-03-20 18:49:59 +00:00
vector<transmatrix> oris(N);
2018-11-17 19:25:47 +00:00
vector<ld> vels(N);
using shmup::monster;
map<cell*, vector<monster*>> monsat;
for(int i=0; i<N; i++) {
vertexdata& vd = vdata[i];
auto m = vd.m;
monsat[m->base].push_back(m);
}
lines.clear();
if(swarm) for(int i=0; i<N; i++) {
vertexdata& vd = vdata[i];
auto m = vd.m;
2022-12-18 13:06:38 +00:00
apply_shift_object(m->at, m->ori, xtangent(0.01)); // max_speed * d));
fixmatrix(m->at);
2018-11-17 19:25:47 +00:00
virtualRebase(m);
}
if(!swarm) parallelize(N, [&monsat, &d, &vels, &pats, &oris] (int a, int b) { for(int i=a; i<b; i++) {
2018-11-17 19:25:47 +00:00
vertexdata& vd = vdata[i];
auto m = vd.m;
2020-03-20 18:49:59 +00:00
transmatrix I, Rot;
2020-03-21 08:34:50 +00:00
bool use_rot = true;
2020-03-20 18:49:59 +00:00
if(mproduct) {
2020-03-21 08:34:50 +00:00
I = inverse(m->at);
Rot = inverse(m->ori);
}
else if(nonisotropic) {
2020-03-20 18:49:59 +00:00
I = gpushxto0(tC0(m->at));
Rot = inverse(I * m->at);
}
else {
I = inverse(m->at);
Rot = Id;
2020-03-21 08:34:50 +00:00
use_rot = false;
2020-03-20 18:49:59 +00:00
}
2018-11-17 19:25:47 +00:00
// we do all the computations here in the frame of reference
// where m is at (0,0,1) and its velocity is (m->vel,0,0)
hyperpoint velvec = hpxyz(m->vel, 0, 0);
2018-11-17 19:25:47 +00:00
hyperpoint sep = hpxyz(0, 0, 0);
int sep_count = 0;
hyperpoint align = hpxyz(0, 0, 0);
int align_count = 0;
hyperpoint coh = hpxyz(0, 0, 0);
int coh_count = 0;
for(auto& p: relmatrices[m->base]) {
auto f = monsat.find(p.first);
if(f != monsat.end()) for(auto m2: f->second) if(m != m2) {
2018-11-17 19:25:47 +00:00
ld vel2 = m2->vel;
transmatrix at2 = I * p.second * m2->at;
// at2 is like m2->at but relative to m->at
// m2's position relative to m (tC0 means *(0,0,1))
2020-07-29 21:34:00 +00:00
hyperpoint ac = inverse_exp(shiftless(tC0(at2)));
2020-03-21 08:34:50 +00:00
if(use_rot) ac = Rot * ac;
// distance and azimuth to m2
2020-03-20 18:49:59 +00:00
ld di = hypot_d(WDIM, ac);
2018-11-17 19:25:47 +00:00
color_t col = 0;
if(di < align_range) {
// we need to transfer m2's velocity vector to m's position
// this is done by applying an isometry which sends m2 to m1
// and maps the straight line on which m1 and m2 are to itself
2020-03-20 18:49:59 +00:00
// note: in nonisotropic it is not clear whether we should
// use gpushxto0, or parallel transport along the shortest geodesic
align += gpushxto0(tC0(at2)) * at2 * hpxyz(vel2, 0, 0);
2018-11-17 19:25:47 +00:00
align_count++;
col |= 0xFF0040;
2018-11-17 19:25:47 +00:00
}
if(di < coh_range) {
2020-03-20 18:49:59 +00:00
coh += tangent_length(ac, di);
2018-11-17 19:25:47 +00:00
coh_count++;
col |= 0xFF40;
2018-11-17 19:25:47 +00:00
}
if(di < sep_range && di > 0) {
2020-03-20 18:49:59 +00:00
sep -= tangent_length(ac, 1 / di);
sep_count++;
col |= 0xFF000040;
2018-11-17 19:25:47 +00:00
}
if(col && draw_lines)
lines.emplace_back(m->pat * C0, m->pat * at2 * C0, col);
}
}
// a bit simpler rules than original
2018-11-17 19:25:47 +00:00
if(sep_count) velvec += sep * (d * sep_factor / sep_count);
if(align_count) velvec += align * (d * align_factor / align_count);
if(coh_count) velvec += coh * (d * coh_factor / coh_count);
// hypot2 is the length of a vector in R^2
vels[i] = hypot_d(2, velvec);
transmatrix alphaspin = rspintox(velvec); // spin(-atan2(velvec));
2018-11-17 19:25:47 +00:00
if(vels[i] > max_speed) {
velvec = velvec * (max_speed / vels[i]);
vels[i] = max_speed;
}
2020-03-20 18:49:59 +00:00
pats[i] = m->at;
oris[i] = m->ori;
rotate_object(pats[i], oris[i], alphaspin);
2022-12-18 13:06:38 +00:00
apply_shift_object(pats[i], oris[i], xtangent(vels[i] * d));
2019-04-21 18:48:15 +00:00
fixmatrix(pats[i]);
2020-03-21 08:34:50 +00:00
/* RogueViz does not correctly rotate them */
if(mproduct) {
2020-03-21 08:34:50 +00:00
hyperpoint h = oris[i] * xtangent(1);
pats[i] = pats[i] * spin(-atan2(h[1], h[0]));
oris[i] = spin(+atan2(h[1], h[0])) * oris[i];
}
} return 0; });
if(!swarm) for(int i=0; i<N; i++) {
2018-11-17 19:25:47 +00:00
vertexdata& vd = vdata[i];
auto m = vd.m;
// these two functions compute new base and at, based on pats[i]
2019-04-21 18:48:15 +00:00
m->at = pats[i];
2020-03-20 18:49:59 +00:00
m->ori = oris[i];
2019-11-14 19:26:07 +00:00
virtualRebase(m);
2018-11-17 19:25:47 +00:00
m->vel = vels[i];
}
shmup::fixStorage();
2018-11-19 17:59:22 +00:00
2018-11-17 19:25:47 +00:00
}
bool turn(int delta) {
simulate(delta), timetowait = 0;
if(follow) {
2018-11-19 17:59:22 +00:00
if(follow == 1) {
gmatrix.clear();
2020-07-29 21:34:00 +00:00
vdata[0].m->pat = shiftless(View * calc_relative_matrix(vdata[0].m->base, centerover, C0) * vdata[0].m->at);
View = inverse(vdata[0].m->pat.T) * View;
if(mproduct) {
2020-03-21 08:34:50 +00:00
NLP = inverse(vdata[0].m->ori);
NLP = hr::cspin90(1, 2) * spin90() * NLP;
2020-03-21 08:34:50 +00:00
if(NLP[0][2]) {
auto downspin = -atan2(NLP[0][2], NLP[1][2]);
NLP = spin(downspin) * NLP;
}
}
else {
View =spin90() * View;
2020-03-21 08:34:50 +00:00
if(GDIM == 3) {
View = hr::cspin90(1, 2) * View;
2020-03-21 08:34:50 +00:00
}
shift_view(ztangent(follow_dist));
2020-03-21 08:34:50 +00:00
}
}
if(follow == 2) {
// we take the average in R^3 of all the boid positions of the Minkowski hyperboloid
// (in quotient spaces, the representants closest to the current view
// are taken), and normalize the result to project it back to the hyperboloid
// (the same method is commonly used on the sphere AFAIK)
hyperpoint h = Hypc;
2020-03-21 08:34:50 +00:00
int cnt = 0;
ld lev = 0;
for(int i=0; i<N; i++) if(gmatrix.count(vdata[i].m->base)) {
vdata[i].m->pat = gmatrix[vdata[i].m->base] * vdata[i].m->at;
2020-07-29 21:34:00 +00:00
auto h1 = unshift(tC0(vdata[i].m->pat));
2020-03-21 08:34:50 +00:00
cnt++;
if(mproduct) {
2020-03-21 08:34:50 +00:00
auto d1 = product_decompose(h1);
lev += d1.first;
h += d1.second;
}
else
h += h1;
}
2020-03-21 08:34:50 +00:00
if(cnt) {
h = cgi.emb->normalize_flat(h);
if(mproduct) h = orthogonal_move(h, lev / cnt);
View = inverse(actual_view_transform) * gpushxto0(h) * actual_view_transform * View;
shift_view(ztangent(follow_dist));
}
}
2018-11-19 17:59:22 +00:00
optimizeview();
compute_graphical_distance();
gmatrix.clear();
2018-11-19 17:59:22 +00:00
playermoved = false;
}
2018-11-17 19:25:47 +00:00
return false;
// shmup::pc[0]->rebase();
}
#if CAP_COMMANDLINE
int readArgs() {
using namespace arg;
// options before reading
if(0) ;
else if(argis("-flocking")) {
2018-12-13 16:03:52 +00:00
PHASEFROM(2);
shift(); N = argi(); swarm = false;
init();
}
else if(argis("-swarming")) {
PHASEFROM(2);
shift(); N = argi(); swarm = true;
init();
}
else if(argis("-flocktails")) {
PHASEFROM(2);
draw_tails = true;
2018-11-17 19:25:47 +00:00
init();
}
else if(argis("-cohf")) {
shift(); coh_factor = argf();
}
else if(argis("-alignf")) {
shift(); align_factor = argf();
}
else if(argis("-sepf")) {
shift(); sep_factor = argf();
}
else if(argis("-checkr")) {
shift(); check_range = argf();
}
else if(argis("-cohr")) {
shift(); coh_range = argf();
}
else if(argis("-alignr")) {
shift(); align_range = argf();
}
else if(argis("-sepr")) {
shift(); sep_range = argf();
}
else if(argis("-flockfollow")) {
shift(); follow = argi();
}
else if(argis("-flockprec")) {
shift(); precision = argi();
}
else if(argis("-flockshape")) {
shift(); shape = argcs()[0];
for(int i=0; i<N; i++)
vdata[i].cp.shade = shape;
}
2020-03-20 18:50:23 +00:00
else if(argis("-flockspd")) {
shift(); ini_speed = argf();
shift(); max_speed = argf();
}
else if(argis("-threads")) {
shift(); threads = argi();
}
2018-11-17 19:25:47 +00:00
else return 1;
return 0;
}
void flock_marker() {
if(draw_lines)
for(auto p: lines) queueline(get<0>(p), get<1>(p), get<2>(p), 0);
}
void show() {
2018-12-13 16:02:10 +00:00
cmode = sm::SIDE | sm::MAYDARK;
2022-07-05 14:03:12 +00:00
gamescreen();
2018-11-17 19:25:47 +00:00
dialog::init(XLAT("flocking"), iinf[itPalace].color, 150, 0);
dialog::addSelItem("initial speed", fts(ini_speed), 'i');
dialog::add_action([]() {
dialog::editNumber(ini_speed, 0, 2, .1, .5, "", "");
});
dialog::addSelItem("max speed", fts(max_speed), 'm');
dialog::add_action([]() {
dialog::editNumber(max_speed, 0, 2, .1, .5, "", "");
});
dialog::addSelItem("separation factor", fts(sep_factor), 's');
dialog::add_action([]() {
dialog::editNumber(sep_factor, 0, 2, .1, 1.5, "", "");
});
string rangehelp = "Increasing this parameter may also require increasing the 'check range' parameter.";
2018-11-17 19:25:47 +00:00
dialog::addSelItem("separation range", fts(sep_range), 'S');
dialog::add_action([rangehelp]() {
dialog::editNumber(sep_range, 0, 2, .1, .5, "", rangehelp);
2018-11-17 19:25:47 +00:00
});
dialog::addSelItem("alignment factor", fts(align_factor), 'a');
dialog::add_action([]() {
dialog::editNumber(align_factor, 0, 2, .1, 1.5, "", "");
});
dialog::addSelItem("alignment range", fts(align_range), 'A');
dialog::add_action([rangehelp]() {
dialog::editNumber(align_range, 0, 2, .1, .5, "", rangehelp);
2018-11-17 19:25:47 +00:00
});
dialog::addSelItem("cohesion factor", fts(coh_factor), 'c');
dialog::add_action([]() {
dialog::editNumber(coh_factor, 0, 2, .1, 1.5, "", "");
});
dialog::addSelItem("cohesion range", fts(coh_range), 'C');
dialog::add_action([rangehelp]() {
dialog::editNumber(coh_range, 0, 2, .1, .5, "", rangehelp);
2018-11-17 19:25:47 +00:00
});
dialog::addSelItem("check range", fts(check_range), 't');
dialog::add_action([]() {
ld radius = 0;
for(cell *c: currentmap->allcells())
for(int i=0; i<c->degree(); i++) {
hyperpoint h = nearcorner(c, i);
radius = max(radius, hdist0(h));
}
dialog::editNumber(check_range, 0, 2, .1, .5, "",
"Value used in the algorithm: "
"only other boids in cells whose centers are at most 'check range' from the center of the current cell are considered. "
"Should be more than the other ranges by at least double the cell radius (in the current geometry, double the radius is " + fts(radius*2) + "); "
"but too large values slow the simulation down.\n\n"
"Restart the simulation to apply the changes to this parameter. In quotient spaces, the simulation may not work correctly when the same cell is in range check_range "
"in multiple directions."
2018-11-17 19:25:47 +00:00
);
});
dialog::addSelItem("number of boids", its(N), 'n');
dialog::add_action([]() {
dialog::editNumber(N, 0, 1000, 1, 20, "", "");
});
dialog::addSelItem("precision", its(precision), 'p');
dialog::add_action([]() {
dialog::editNumber(precision, 0, 1000, 1, 10, "", "smaller number = more precise simulation");
});
2019-02-08 16:08:00 +00:00
dialog::addSelItem("change geometry", XLAT(ginf[geometry].shortname), 'g');
hr::showquotients = true;
2018-11-17 19:25:47 +00:00
dialog::add_action(runGeometryExperiments);
dialog::addBoolItem_action("draw forces", draw_lines, 'l');
dialog::addBoolItem_action("draw tails", draw_tails, 't');
2018-11-17 19:25:47 +00:00
2018-11-19 17:59:22 +00:00
dialog::addSelItem("follow", follow_names[follow], 'f');
dialog::add_action([] () { follow++; follow %= 3; });
dialog::addSelItem("follow distance", fts(follow_dist), 'd');
dialog::add_action([] () {
dialog::editNumber(follow_dist, -1, 1, 0.1, 0, "follow distance", "");
follow++; follow %= 3;
});
2018-11-19 17:59:22 +00:00
2018-11-17 19:25:47 +00:00
dialog::addBreak(100);
dialog::addItem("restart", 'r');
dialog::add_action(init);
dialog::addBack();
dialog::display();
}
void o_key(o_funcs& v) {
2021-03-30 22:23:01 +00:00
v.push_back(named_dialog("flocking", show));
2018-11-17 19:25:47 +00:00
}
bool drawVertex(const shiftmatrix &V, cell *c, shmup::monster *m) {
if(draw_tails) {
int i = m->pid;
vertexdata& vd = vdata[i];
vid.linewidth *= 3;
queueline(V * m->at * C0, V * m->at * xpush0(-3), vd.cp.color2 & 0xFFFFFFF3F, 6);
vid.linewidth /= 3;
}
return false;
}
2021-03-30 22:23:01 +00:00
void init() {
if(!closed_manifold) {
addMessage("Flocking simulation needs a closed manifold.");
2021-03-30 22:23:01 +00:00
return;
}
stop_game();
2021-03-30 23:10:45 +00:00
rogueviz::init(RV_GRAPH);
2021-03-30 22:23:01 +00:00
rv_hook(shmup::hooks_turn, 100, turn);
rv_hook(hooks_frame, 100, flock_marker);
rv_hook(hooks_o_key, 80, o_key);
rv_hook(shmup::hooks_draw, 90, drawVertex);
2021-03-30 22:23:01 +00:00
vdata.resize(N);
const auto v = currentmap->allcells();
printf("computing relmatrices...\n");
// relmatrices[c1][c2] is the matrix we have to multiply by to
// change from c1-relative coordinates to c2-relative coordinates
for(cell* c1: v) {
manual_celllister cl;
cl.add(c1);
for(int i=0; i<isize(cl.lst); i++) {
cell *c2 = cl.lst[i];
transmatrix T = calc_relative_matrix(c2, c1, C0);
if(hypot_d(WDIM, inverse_exp(shiftless(tC0(T)))) <= check_range) {
relmatrices[c1][c2] = T;
forCellEx(c3, c2) cl.add(c3);
}
}
}
ld angle;
if(swarm) angle = hrand(1000);
2021-03-30 22:23:01 +00:00
printf("setting up...\n");
for(int i=0; i<N; i++) {
vertexdata& vd = vdata[i];
// set initial base and at to random cell and random position there
createViz(i, v[swarm ? 0 : hrand(isize(v))], Id);
vd.m->pat.T = Id;
if(swarm) {
rotate_object(vd.m->pat.T, vd.m->ori, spin(angle));
2022-12-18 13:06:38 +00:00
apply_shift_object(vd.m->pat.T, vd.m->ori, xtangent(i * -0.015));
}
else {
rotate_object(vd.m->pat.T, vd.m->ori, random_spin());
2022-12-18 13:06:38 +00:00
apply_shift_object(vd.m->pat.T, vd.m->ori, xtangent(hrandf() / 2));
rotate_object(vd.m->pat.T, vd.m->ori, random_spin());
}
2021-03-30 22:23:01 +00:00
vd.name = its(i+1);
vd.cp = dftcolor;
if(swarm)
vd.cp.color2 =
(rainbow_color(0.5, i * 1. / N) << 8) | 0xFF;
else
vd.cp.color2 =
((hrand(0x1000000) << 8) + 0xFF) | 0x808080FF;
2021-03-30 22:23:01 +00:00
vd.cp.shade = shape;
vd.m->vel = ini_speed;
vd.m->at = vd.m->pat.T;
2021-03-30 22:23:01 +00:00
}
storeall();
printf("done\n");
}
2021-04-07 12:49:20 +00:00
void set_follow() {
follow = (1+follow) % 3;
addMessage("following: " + follow_names[follow]);
}
void flock_slide(tour::presmode mode, int _N, reaction_t t) {
using namespace tour;
setWhiteCanvas(mode);
2021-04-07 12:49:20 +00:00
if(mode == pmStart) {
slide_backup(mapeditor::drawplayer);
2021-04-07 12:49:20 +00:00
t();
slide_backup(rogueviz::vertex_shape, 3);
N = _N; start_game(); init();
}
if(mode == pmKey) set_follow();
}
auto hooks = addHook(hooks_args, 100, readArgs)
+ addHook_rvslides(187, [] (string s, vector<tour::slide>& v) {
2021-04-07 12:49:20 +00:00
if(s != "mixed") return;
using namespace tour;
string cap = "flocking simulation/";
string help = "\n\nPress '5' to make the camera follow boids, or 'o' to change more parameters.";
v.push_back(slide{
cap+"Euclidean flocking", 10, LEGAL::NONE | QUICKGEO,
"This is an Euclidean flocking simulation. Boids move according to the following rules:\n\n"
"- separation: they avoid running into other boids\n"
"- alignment: steer toward the average heading of local flockmates\n"
"- cohesion: steer toward the average position of local flockmates\n\n"
"In the Euclidean space, these rules will cause all the boids to align, and fly in the same direction in a nice flock."+help
,
[] (presmode mode) {
slide_url(mode, 'w', "Wikipedia link", "https://en.wikipedia.org/wiki/Boids");
2021-04-07 12:49:20 +00:00
flock_slide(mode, 50, [] {
set_geometry(gEuclid);
set_variation(eVariation::bitruncated);
auto& T0 = euc::eu_input.user_axes;
restorers.push_back([] { euc::build_torus3(); });
slide_backup(euc::eu_input);
T0[0][0] = T0[1][1] = 3;
T0[1][0] = T0[0][1] = 0;
euc::eu_input.twisted = 0;
euc::build_torus3();
});
}});
v.push_back(slide{
cap+"spherical flocking", 10, LEGAL::NONE | QUICKGEO,
"Same parameters, but in spherical geometry.\n\n"
"Since parallel lines work differently, the boids do not align that nicely. "
"However, the curvature helps them to maintain a coherent flock."
+help
,
[] (presmode mode) {
flock_slide(mode, 50, [] {
set_geometry(gSphere);
set_variation(eVariation::bitruncated);
});
}});
v.push_back(slide{
cap+"Hyperbolic flocking", 10, LEGAL::NONE | QUICKGEO,
"Same parameters, but the geometry is hyperbolic. Our boids fly in the Klein quartic.\n"
"This time, negative curvature prevents our boids from maintaining a coherent flock."
+help
,
[] (presmode mode) {
flock_slide(mode, 50, [] {
set_geometry(gKleinQuartic);
set_variation(eVariation::bitruncated);
});
}});
v.push_back(slide{
cap+"Hyperbolic flocking again", 10, LEGAL::NONE | QUICKGEO,
"Our boids still fly in the Klein quartic, but now the parameters are changed to "
"make the alignment and cohesion stronger."
,
[] (presmode mode) {
slide_url(mode, 't', "Twitter link", "https://twitter.com/ZenoRogue/status/1064660283581505536");
flock_slide(mode, 50, [] {
set_geometry(gKleinQuartic);
set_variation(eVariation::bitruncated);
slide_backup(align_factor, 2);
slide_backup(align_range, 2);
slide_backup(coh_factor, 2);
});
}});
v.push_back(slide{
cap+"Hyperbolic flocking in 3D", 10, LEGAL::NONE | QUICKGEO,
"Let's try a three-dimensional hyperbolic manifold. Alignment and cohesion are strong again."
,
[] (presmode mode) {
slide_url(mode, 'y', "YouTube link", "https://www.youtube.com/watch?v=kng_4lE0uzo");
flock_slide(mode, 50, [] {
set_geometry(gSpace534);
field_quotient_3d(5, 0x72414D0C);
slide_backup(align_factor, 2);
slide_backup(align_range, 2);
slide_backup(coh_factor, 2);
slide_backup(vid.grid, true);
slide_backup(follow_dist, 1);
});
}});
});
2018-11-17 19:25:47 +00:00
#endif
}
}