]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/common/util.qc
Disambiguate a parameter name in a few functions
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / util.qc
index ae0c2ae77ae5c49e6aa00ef3c5b42a0035783be6..3fb2d7e1af3e712a27788d00004b11b5057ae9bb 100644 (file)
@@ -1,22 +1,22 @@
 #include "util.qh"
 
 #if defined(CSQC)
-    #include "constants.qh"
        #include <client/mutators/_mod.qh>
-    #include "mapinfo.qh"
-    #include "notifications/all.qh"
-       #include "scores.qh"
-    #include <common/deathtypes/all.qh>
+       #include <common/constants.qh>
+       #include <common/deathtypes/all.qh>
        #include <common/gamemodes/_mod.qh>
+       #include <common/mapinfo.qh>
+       #include <common/notifications/all.qh>
+       #include <common/scores.qh>
 #elif defined(MENUQC)
 #elif defined(SVQC)
-    #include "constants.qh"
-       #include <server/mutators/_mod.qh>
-    #include "notifications/all.qh"
-    #include <common/deathtypes/all.qh>
+       #include <common/constants.qh>
+       #include <common/deathtypes/all.qh>
        #include <common/gamemodes/_mod.qh>
-       #include "scores.qh"
-    #include "mapinfo.qh"
+       #include <common/mapinfo.qh>
+       #include <common/notifications/all.qh>
+       #include <common/scores.qh>
+       #include <server/mutators/_mod.qh>
 #endif
 
 #ifdef SVQC
@@ -210,6 +210,70 @@ string draw_UseSkinFor(string pic)
        else
                return strcat(draw_currentSkin, "/", pic);
 }
+
+void mut_set_active(int mut)
+{
+       if (mut >= 24)
+               active_mutators[1] |= BIT(mut - 24);
+       else
+               active_mutators[0] |= BIT(mut);
+}
+
+bool mut_is_active(int mut)
+{
+       if (mut >= 24)
+               return (active_mutators[1] & (BIT(mut - 24)));
+       else
+               return (active_mutators[0] & BIT(mut));
+}
+
+// if s == "" (MENUQC) builds the mutator list for the Mutators dialog based on local cvar values
+// otherwise (CSQC) translates the mutator list (s) that client has received from server
+// NOTE: this function merges MENUQC and CSQC code in order to avoid duplicating and separating strings
+string build_mutator_list(string s)
+{
+       int i = -1, n = 0; // allow only 1 iteration in the following for loop if (s == "")
+       if (s != "")
+       {
+               i = 0;
+               n = tokenizebyseparator(s, ", ");
+       }
+       string s2 = "";
+       for (string arg = ""; i < n; i++)
+       {
+               if (i >= 0) arg = argv(i);
+               // cond is the condition for showing the mutator enabled in the menu
+               #define X(name, translated_name, mut, cond) \
+                       if(arg == name || (!n && (cond))) { s2 = cons_mid(s2, ", ", translated_name); mut_set_active(mut); }
+               X("Dodging"                   , _("Dodging")                   , MUT_DODGING                   , cvar("g_dodging"))
+               X("InstaGib"                  , _("InstaGib")                  , MUT_INSTAGIB                  , cvar("g_instagib"))
+               X("New Toys"                  , _("New Toys")                  , MUT_NEW_TOYS                  , cvar("g_new_toys"))
+               X("NIX"                       , _("NIX")                       , MUT_NIX                       , cvar("g_nix"))
+               X("Rocket Flying"             , _("Rocket Flying")             , MUT_ROCKET_FLYING             , cvar("g_rocket_flying"))
+               X("Invincible Projectiles"    , _("Invincible Projectiles")    , MUT_INVINCIBLE_PROJECTILES    , cvar("g_invincible_projectiles"))
+               X("Low gravity"               , _("Low gravity")               , MUT_GRAVITY                   , cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
+               X("Cloaked"                   , _("Cloaked")                   , MUT_CLOAKED                   , cvar("g_cloaked"))
+               X("Hook"                      , _("Hook")                      , MUT_GRAPPLING_HOOK            , cvar("g_grappling_hook"))
+               X("Midair"                    , _("Midair")                    , MUT_MIDAIR                    , cvar("g_midair"))
+               X("Melee only Arena"          , _("Melee only Arena")          , MUT_MELEE_ONLY                , cvar("g_melee_only"))
+               X("Vampire"                   , _("Vampire")                   , MUT_VAMPIRE                   , cvar("g_vampire"))
+               X("Piñata"                    , _("Piñata")                    , MUT_PINATA                    , cvar("g_pinata"))
+               X("Weapons stay"              , _("Weapons stay")              , MUT_WEAPON_STAY               , cvar("g_weapon_stay"))
+               X("Blood loss"                , _("Blood loss")                , MUT_BLOODLOSS                 , cvar("g_bloodloss") > 0)
+               X("Jetpack"                   , _("Jetpack")                   , MUT_JETPACK                   , cvar("g_jetpack"))
+               X("Buffs"                     , _("Buffs")                     , MUT_BUFFS                     , cvar("g_buffs") > 0)
+               X("Overkill"                  , _("Overkill")                  , MUT_OVERKILL                  , cvar("g_overkill"))
+               X("No powerups"               , _("No powerups")               , MUT_NO_POWERUPS               , cvar("g_powerups") == 0)
+               X("Powerups"                  , _("Powerups")                  , MUT_POWERUPS                  , cvar("g_powerups") > 0)
+               X("Touch explode"             , _("Touch explode")             , MUT_TOUCHEXPLODE              , cvar("g_touchexplode") > 0)
+               X("Wall jumping"              , _("Wall jumping")              , MUT_WALLJUMP                  , cvar("g_walljump"))
+               X("No start weapons"          , _("No start weapons")          , MUT_NO_START_WEAPONS          , cvar_string("g_weaponarena") == "0" && cvar("g_balance_blaster_weaponstartoverride") == 0)
+               X("Nades"                     , _("Nades")                     , MUT_NADES                     , cvar("g_nades"))
+               X("Offhand blaster"           , _("Offhand blaster")           , MUT_OFFHAND_BLASTER           , cvar("g_offhand_blaster"))
+               #undef X
+       }
+       return s2;
+}
 #endif
 
 void wordwrap_cb(string s, float l, void(string) callback)
@@ -318,9 +382,9 @@ string ScoreString(int pFlags, float pValue)
        if((pValue == 0) && (pFlags & (SFL_HIDE_ZERO | SFL_RANK | SFL_TIME)))
                valstr = "";
        else if(pFlags & SFL_RANK)
-               valstr = count_ordinal(pValue);
+               valstr = (pValue < 256 ? count_ordinal(pValue) : _("N/A"));
        else if(pFlags & SFL_TIME)
-               valstr = TIME_ENCODED_TOSTRING(pValue);
+               valstr = TIME_ENCODED_TOSTRING(pValue, true);
        else
                valstr = ftos(pValue);
 
@@ -770,57 +834,6 @@ int cvar_settemp_restore()
        return j;
 }
 
-bool isCaretEscaped(string theText, float pos)
-{
-       int i = 0;
-       while(pos - i >= 1 && substring(theText, pos - i - 1, 1) == "^")
-               ++i;
-       return (i & 1);
-}
-
-int skipIncompleteTag(string theText, float pos, int len)
-{
-       int tag_start = -1;
-
-       if(substring(theText, pos - 1, 1) == "^")
-       {
-               if(isCaretEscaped(theText, pos - 1) || pos >= len)
-                       return 0;
-
-               int ch = str2chr(theText, pos);
-               if(ch >= '0' && ch <= '9')
-                       return 1; // ^[0-9] color code found
-               else if (ch == 'x')
-                       tag_start = pos - 1; // ^x tag found
-               else
-                       return 0;
-       }
-       else
-       {
-               for(int i = 2; pos - i >= 0 && i <= 4; ++i)
-               {
-                       if(substring(theText, pos - i, 2) == "^x")
-                       {
-                               tag_start = pos - i; // ^x tag found
-                               break;
-                       }
-               }
-       }
-
-       if(tag_start >= 0)
-       {
-               if(tag_start + 5 < len)
-               if(IS_HEXDIGIT(substring(theText, tag_start + 2, 1)))
-               if(IS_HEXDIGIT(substring(theText, tag_start + 3, 1)))
-               if(IS_HEXDIGIT(substring(theText, tag_start + 4, 1)))
-               {
-                       if(!isCaretEscaped(theText, tag_start))
-                               return 5 - (pos - tag_start); // ^xRGB color code found
-               }
-       }
-       return 0;
-}
-
 float textLengthUpToWidth(string theText, float maxWidth, vector theSize, textLengthUpToWidth_widthFunction_t w)
 {
        // STOP.
@@ -842,7 +855,11 @@ float textLengthUpToWidth(string theText, float maxWidth, vector theSize, textLe
        {
                middle = floor((left + right) / 2);
                if(colors)
-                       ofs = skipIncompleteTag(theText, middle, len);
+               {
+                       vector res = checkColorCode(theText, len, middle, false);
+                       ofs = (res.x) ? res.x - res.y : 0;
+               }
+
                if(w(substring(theText, 0, middle + ofs), theSize) <= maxWidth)
                        left = middle + ofs;
                else
@@ -853,14 +870,14 @@ float textLengthUpToWidth(string theText, float maxWidth, vector theSize, textLe
        return left;
 }
 
-float textLengthUpToLength(string theText, float maxWidth, textLengthUpToLength_lenFunction_t w)
+float textLengthUpToLength(string theText, int maxLength, textLengthUpToLength_lenFunction_t w)
 {
        // STOP.
        // The following function is SLOW.
        // For your safety and for the protection of those around you...
        // DO NOT CALL THIS AT HOME.
        // No really, don't.
-       if(w(theText) <= maxWidth)
+       if(w(theText) <= maxLength)
                return strlen(theText); // yeah!
 
        bool colors = (w("^7") == 0);
@@ -874,8 +891,12 @@ float textLengthUpToLength(string theText, float maxWidth, textLengthUpToLength_
        {
                middle = floor((left + right) / 2);
                if(colors)
-                       ofs = skipIncompleteTag(theText, middle, len);
-               if(w(substring(theText, 0, middle + ofs)) <= maxWidth)
+               {
+                       vector res = checkColorCode(theText, len, middle, true);
+                       ofs = (!res.x) ? 0 : res.x - res.y;
+               }
+
+               if(w(substring(theText, 0, middle + ofs)) <= maxLength)
                        left = middle + ofs;
                else
                        right = middle;
@@ -920,44 +941,36 @@ string find_last_color_code(string s)
        return "";
 }
 
-string getWrappedLine(float w, vector theFontSize, textLengthUpToWidth_widthFunction_t tw)
+string getWrappedLine(float maxWidth, vector theFontSize, textLengthUpToWidth_widthFunction_t tw)
 {
-       float cantake;
-       float take;
-       string s;
+       string s = getWrappedLine_remaining;
 
-       s = getWrappedLine_remaining;
-
-       if(w <= 0)
+       if(maxWidth <= 0)
        {
                getWrappedLine_remaining = string_null;
                return s; // the line has no size ANYWAY, nothing would be displayed.
        }
 
-       cantake = textLengthUpToWidth(s, w, theFontSize, tw);
-       if(cantake > 0 && cantake < strlen(s))
+       int take_until = textLengthUpToWidth(s, maxWidth, theFontSize, tw);
+       if(take_until > 0 && take_until < strlen(s))
        {
-               take = cantake - 1;
-               while(take > 0 && substring(s, take, 1) != " ")
-                       --take;
-               if(take == 0)
-               {
-                       getWrappedLine_remaining = substring(s, cantake, strlen(s) - cantake);
-                       if(getWrappedLine_remaining == "")
-                               getWrappedLine_remaining = string_null;
-                       else if (tw("^7", theFontSize) == 0)
-                               getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, cantake)), getWrappedLine_remaining);
-                       return substring(s, 0, cantake);
-               }
-               else
+               int last_word = take_until - 1;
+               while(last_word > 0 && substring(s, last_word, 1) != " ")
+                       --last_word;
+
+               int skip = 0;
+               if(last_word != 0)
                {
-                       getWrappedLine_remaining = substring(s, take + 1, strlen(s) - take);
-                       if(getWrappedLine_remaining == "")
-                               getWrappedLine_remaining = string_null;
-                       else if (tw("^7", theFontSize) == 0)
-                               getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, take)), getWrappedLine_remaining);
-                       return substring(s, 0, take);
+                       take_until = last_word;
+                       skip = 1;
                }
+
+               getWrappedLine_remaining = substring(s, take_until + skip, strlen(s) - take_until);
+               if(getWrappedLine_remaining == "")
+                       getWrappedLine_remaining = string_null;
+               else if (tw("^7", theFontSize) == 0)
+                       getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, take_until)), getWrappedLine_remaining);
+               return substring(s, 0, take_until);
        }
        else
        {
@@ -966,44 +979,36 @@ string getWrappedLine(float w, vector theFontSize, textLengthUpToWidth_widthFunc
        }
 }
 
-string getWrappedLineLen(float w, textLengthUpToLength_lenFunction_t tw)
+string getWrappedLineLen(int maxLength, textLengthUpToLength_lenFunction_t tw)
 {
-       float cantake;
-       float take;
-       string s;
+       string s = getWrappedLine_remaining;
 
-       s = getWrappedLine_remaining;
-
-       if(w <= 0)
+       if(maxLength <= 0)
        {
                getWrappedLine_remaining = string_null;
                return s; // the line has no size ANYWAY, nothing would be displayed.
        }
 
-       cantake = textLengthUpToLength(s, w, tw);
-       if(cantake > 0 && cantake < strlen(s))
+       int take_until = textLengthUpToLength(s, maxLength, tw);
+       if(take_until > 0 && take_until < strlen(s))
        {
-               take = cantake - 1;
-               while(take > 0 && substring(s, take, 1) != " ")
-                       --take;
-               if(take == 0)
-               {
-                       getWrappedLine_remaining = substring(s, cantake, strlen(s) - cantake);
-                       if(getWrappedLine_remaining == "")
-                               getWrappedLine_remaining = string_null;
-                       else if (tw("^7") == 0)
-                               getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, cantake)), getWrappedLine_remaining);
-                       return substring(s, 0, cantake);
-               }
-               else
+               int last_word = take_until - 1;
+               while(last_word > 0 && substring(s, last_word, 1) != " ")
+                       --last_word;
+
+               int skip = 0;
+               if(last_word != 0)
                {
-                       getWrappedLine_remaining = substring(s, take + 1, strlen(s) - take);
-                       if(getWrappedLine_remaining == "")
-                               getWrappedLine_remaining = string_null;
-                       else if (tw("^7") == 0)
-                               getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, take)), getWrappedLine_remaining);
-                       return substring(s, 0, take);
+                       take_until = last_word;
+                       skip = 1;
                }
+
+               getWrappedLine_remaining = substring(s, take_until + skip, strlen(s) - take_until);
+               if(getWrappedLine_remaining == "")
+                       getWrappedLine_remaining = string_null;
+               else if (tw("^7") == 0)
+                       getWrappedLine_remaining = strcat(find_last_color_code(substring(s, 0, take_until)), getWrappedLine_remaining);
+               return substring(s, 0, take_until);
        }
        else
        {
@@ -1298,7 +1303,7 @@ ERASEABLE
 void write_String_To_File(int fh, string str, bool alsoprint)
 {
        fputs(fh, str);
-       if (alsoprint) LOG_INFO(str);
+       if (alsoprint) LOG_HELP(str);
 }
 
 string get_model_datafilename(string m, float sk, string fil)
@@ -1769,23 +1774,6 @@ Notification Announcer_PickNumber(int type, int num)
                        }
                        break;
                }
-               case CNT_IDLE:
-               {
-                       switch(num)
-                       {
-                               case 10: return ANNCE_NUM_IDLE_10;
-                               case 9:  return ANNCE_NUM_IDLE_9;
-                               case 8:  return ANNCE_NUM_IDLE_8;
-                               case 7:  return ANNCE_NUM_IDLE_7;
-                               case 6:  return ANNCE_NUM_IDLE_6;
-                               case 5:  return ANNCE_NUM_IDLE_5;
-                               case 4:  return ANNCE_NUM_IDLE_4;
-                               case 3:  return ANNCE_NUM_IDLE_3;
-                               case 2:  return ANNCE_NUM_IDLE_2;
-                               case 1:  return ANNCE_NUM_IDLE_1;
-                       }
-                       break;
-               }
                case CNT_KILL:
                {
                        switch(num)
@@ -1837,6 +1825,7 @@ Notification Announcer_PickNumber(int type, int num)
                        }
                        break;
                }
+               case CNT_NORMAL:
                default:
                {
                        switch(num)
@@ -1894,3 +1883,227 @@ int Mod_Q1BSP_NativeContentsFromSuperContents(int supercontents)
        return CONTENT_EMPTY;
 }
 #endif
+
+#ifdef SVQC
+void attach_sameorigin(entity e, entity to, string tag)
+{
+    vector org, t_forward, t_left, t_up, e_forward, e_up;
+    float tagscale;
+
+    org = e.origin - gettaginfo(to, gettagindex(to, tag));
+    tagscale = (vlen(v_forward) ** -2); // undo a scale on the tag
+    t_forward = v_forward * tagscale;
+    t_left = v_right * -tagscale;
+    t_up = v_up * tagscale;
+
+    e.origin_x = org * t_forward;
+    e.origin_y = org * t_left;
+    e.origin_z = org * t_up;
+
+    // current forward and up directions
+    if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
+               e.angles = AnglesTransform_FromVAngles(e.angles);
+       else
+               e.angles = AnglesTransform_FromAngles(e.angles);
+    fixedmakevectors(e.angles);
+
+    // untransform forward, up!
+    e_forward.x = v_forward * t_forward;
+    e_forward.y = v_forward * t_left;
+    e_forward.z = v_forward * t_up;
+    e_up.x = v_up * t_forward;
+    e_up.y = v_up * t_left;
+    e_up.z = v_up * t_up;
+
+    e.angles = fixedvectoangles2(e_forward, e_up);
+    if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
+               e.angles = AnglesTransform_ToVAngles(e.angles);
+       else
+               e.angles = AnglesTransform_ToAngles(e.angles);
+
+    setattachment(e, to, tag);
+    setorigin(e, e.origin);
+}
+
+void detach_sameorigin(entity e)
+{
+    vector org;
+    org = gettaginfo(e, 0);
+    e.angles = fixedvectoangles2(v_forward, v_up);
+    if (substring(e.model, 0, 1) == "*") // bmodels have their own rules
+               e.angles = AnglesTransform_ToVAngles(e.angles);
+       else
+               e.angles = AnglesTransform_ToAngles(e.angles);
+    setorigin(e, org);
+    setattachment(e, NULL, "");
+    setorigin(e, e.origin);
+}
+
+void follow_sameorigin(entity e, entity to)
+{
+    set_movetype(e, MOVETYPE_FOLLOW); // make the hole follow
+    e.aiment = to; // make the hole follow bmodel
+    e.punchangle = to.angles; // the original angles of bmodel
+    e.view_ofs = e.origin - to.origin; // relative origin
+    e.v_angle = e.angles - to.angles; // relative angles
+}
+
+#if 0
+// TODO: unused, likely for a reason, possibly needs extensions (allow setting the new movetype as a parameter?)
+void unfollow_sameorigin(entity e)
+{
+    set_movetype(e, MOVETYPE_NONE);
+}
+#endif
+
+.string aiment_classname;
+.float aiment_deadflag;
+void SetMovetypeFollow(entity ent, entity e)
+{
+       set_movetype(ent, MOVETYPE_FOLLOW); // make the hole follow
+       ent.solid = SOLID_NOT; // MOVETYPE_FOLLOW is always non-solid - this means this cannot be teleported by warpzones any more! Instead, we must notice when our owner gets teleported.
+       ent.aiment = e; // make the hole follow bmodel
+       ent.punchangle = e.angles; // the original angles of bmodel
+       ent.view_ofs = ent.origin - e.origin; // relative origin
+       ent.v_angle = ent.angles - e.angles; // relative angles
+       ent.aiment_classname = e.classname;
+       ent.aiment_deadflag = e.deadflag;
+
+       if(IS_PLAYER(ent.aiment))
+       {
+               entity pl = ent.aiment;
+               ent.view_ofs.x = bound(pl.mins.x + 4, ent.view_ofs.x, pl.maxs.x - 4);
+               ent.view_ofs.y = bound(pl.mins.y + 4, ent.view_ofs.y, pl.maxs.y - 4);
+               ent.view_ofs.z = bound(pl.mins.z + 4, ent.view_ofs.z, pl.maxs.z - 4);
+       }
+}
+
+void UnsetMovetypeFollow(entity ent)
+{
+       set_movetype(ent, MOVETYPE_FLY);
+       PROJECTILE_MAKETRIGGER(ent);
+       ent.aiment_classname = string_null;
+       // FIXME: engine bug?
+       // resetting aiment the engine will set orb's origin close to world's origin
+       //ent.aiment = NULL;
+}
+
+int LostMovetypeFollow(entity ent)
+{
+/*
+       if(ent.move_movetype != MOVETYPE_FOLLOW)
+               if(ent.aiment)
+                       error("???");
+*/
+       // FIXME: engine bug?
+       // when aiment disconnects the engine will set orb's origin close to world's origin
+       if(!ent.aiment)
+               return 2;
+       if(ent.aiment.classname != ent.aiment_classname || ent.aiment.deadflag != ent.aiment_deadflag)
+               return 1;
+       return 0;
+}
+#endif
+
+#ifdef GAMEQC
+// decolorizes and team colors the player name when needed
+string playername(string thename, int teamid, bool team_colorize)
+{
+       TC(int, teamid);
+       bool do_colorize = (teamplay && team_colorize);
+#ifdef SVQC
+       if(do_colorize && !intermission_running)
+#else
+       if(do_colorize)
+#endif
+    {
+        string t = Team_ColorCode(teamid);
+        return strcat(t, strdecolorize(thename));
+    }
+    else
+        return thename;
+}
+
+float trace_hits_box_a0, trace_hits_box_a1;
+
+float trace_hits_box_1d(float end, float thmi, float thma)
+{
+    if (end == 0)
+    {
+        // just check if x is in range
+        if (0 < thmi)
+            return false;
+        if (0 > thma)
+            return false;
+    }
+    else
+    {
+        // do the trace with respect to x
+        // 0 -> end has to stay in thmi -> thma
+        trace_hits_box_a0 = max(trace_hits_box_a0, min(thmi / end, thma / end));
+        trace_hits_box_a1 = min(trace_hits_box_a1, max(thmi / end, thma / end));
+        if (trace_hits_box_a0 > trace_hits_box_a1)
+            return false;
+    }
+    return true;
+}
+
+float trace_hits_box(vector start, vector end, vector thmi, vector thma)
+{
+    end -= start;
+    thmi -= start;
+    thma -= start;
+    // now it is a trace from 0 to end
+
+    trace_hits_box_a0 = 0;
+    trace_hits_box_a1 = 1;
+
+    if (!trace_hits_box_1d(end.x, thmi.x, thma.x))
+        return false;
+    if (!trace_hits_box_1d(end.y, thmi.y, thma.y))
+        return false;
+    if (!trace_hits_box_1d(end.z, thmi.z, thma.z))
+        return false;
+
+    return true;
+}
+
+float tracebox_hits_box(vector start, vector mi, vector ma, vector end, vector thmi, vector thma)
+{
+    return trace_hits_box(start, end, thmi - ma, thma - mi);
+}
+#endif
+
+ERASEABLE
+float cvar_or(string cv, float v)
+{
+       string s = cvar_string(cv);
+       if(s == "")
+               return v;
+       else
+               return stof(s);
+}
+
+// NOTE base is the central value
+// freq: circle frequency, = 2*pi*frequency in hertz
+// start_pos:
+//  -1 start from the lower value
+//   0 start from the base value
+//   1 start from the higher value
+ERASEABLE
+float blink_synced(float base, float range, float freq, float start_time, int start_pos)
+{
+       // note:
+       //   RMS = sqrt(base^2 + 0.5 * range^2)
+       // thus
+       //   base = sqrt(RMS^2 - 0.5 * range^2)
+       // ensure RMS == 1
+
+       return base + range * sin((time - start_time - (M_PI / 2) * start_pos) * freq);
+}
+
+ERASEABLE
+float blink(float base, float range, float freq)
+{
+       return blink_synced(base, range, freq, 0, 0);
+}