1
0
Fork 0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-11-04 18:29:41 +01:00
Commit graph

58 commits

Author SHA1 Message Date
Dav999-v
3e36bfd56f Simplify time formatting functions
Here's my notes on all the existing functions and what kind of time
formats they output:

- Game::giventimestring(int hrs, int min, int sec)
	H:MM:SS
	MM:SS

- Game::timestring()
// uses game.hours/minutes/seconds
	H:MM:SS
	MM:SS

- Game::partimestring()
// uses game.timetrialpar (seconds)
	MM:SS

- Game::resulttimestring()
// uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s)
	MM:SS.CC

- Game::timetstring(int t)
// t = seconds
	MM:SS

- Game::timestringcenti(char* buffer, const size_t buffer_size)
// uses game.hours/minutes/seconds/frames
	H:MM:SS.CC
	MM:SS.CC

- UtilityClass::timestring(int t)
// t = frames, 30 frames = 1 second
	S:CC
	M:SS:CC

This is kind of a mess, and there's a lot of functions that do the same
thing except using different variables. For localization, I also want
translators to be able to localize all these time formats - many
languages use the decimal comma instead of the decimal point (12:34,56)
maybe some languages really prefer something like 1時02分11秒44瞬...
Which I don't know to be correct, but it's good to be prepared for it
and not restrict translators arbitrarily to only changing ":" and "."
when we can start making the system better in the first place.

I added a new function, UtilityClass::format_time. This is the place
where all time formats come together, given the number of seconds and
optionally frames. I have simplified the above-mentioned functions
somewhat, but I haven't given them a complete refactor or renaming -
I mainly made sure that they all use the same backend so I can make the
formats consistent and properly localizable.

(And before we start shoving more temporary char buffers everywhere
just to get rid of the std::string's, maybe we need to think of a
globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a
register of sorts, for when any line of text needs to be made or
processed, then printed, and then goes unused. Maybe help.textrow,
or something like that.)

As for this commit, the available time formats are now more consistent
and changed a little in some places. Leading zeroes for the first unit
are now no longer included, time trial results and the Super Gravitron
can now display hours when they went to 60 minutes before, and we now
always use .CC instead of :CC. These are the formats:
- H:MM:SS
- H:MM:SS.CC
- M:SS
- M:SS.CC
- S.CC  (only used when always_minutes=false, for the Gravitrons)

Here's what changes to the current functions:
- Game::partimestring() is removed - it was used in two places, and
  could be replaced by game.timetstring(game.timetrialpar)
- Game::giventimestring(h,m,s) and Game::timestring() are now wrappers
  for the other functions
- The four remaining functions (Game::resulttimestring(),
  Game::timetstring(t), Game::timestringcenti(buffer, buffer_size)
  and UtilityClass::timestring(t)) are now wrappers for the "central
  function", UtilityClass::format_time.
- UtilityClass::twodigits(int t) is now unused so it's also removed.
- I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 11:38:12 -08:00
Misa
aa7b63fa5f Remove VVV_min/max in favor of SDL_min/max
VVV_min/max are functions that only operate on ints, and SDL_min/max are
macros that operate on any type but double-evaluate everything.

I know I more-or-less said earlier that SDL_min/max were dumb but I've
changed my mind and think it's better to use them, taking care to make
sure you don't double-evaluate, rather than trying to generate your own
litany of functions with either your own hand-rolled generation macros,
C++ templates, C11 generics, or GCC extensions (that last one you'd
technically use in a macro but it doesn't really matter), all of which
have more downsides than just not double-evaluating.

And the upside of not double-evaluating is that you're disencouraged
from having really complicated single-line min/max expressions and
encouraged to precompute the values beforehand anyway so the final
min/max is more readable. And furthermore you'll notice when you
yourself end up doing double-evaluations anyway. I removed a couple
instances of Graphics::len() being double-evaluated in this commit (as
well as cleaned up some other min/max-using code). Although the only
downside to those double-evaluations was unnecessary computation,
rather than checking the wrong result or having multiple side effects,
thankfully, it's still good to minimize double-evaluations where
possible.
2021-12-22 16:43:31 -08:00
Misa
38b2213745 Rename number to number_words
This is to clarify that it returns the word forms of numbers, not
numbers themselves.
2021-09-25 15:08:13 -07:00
Misa
891ca527f9 Remove overcomplicated integer divisions
Believe it or not, there are still some remnants of the ActionScript
coding standards in the codebase! And one of them sometimes pops up
whenever an integer division happens.

As it so happens, it seems like division in ActionScript automatically
produces a decimal number. So to prevent that, the game sometimes
subtracts off the remainder of the number to be divided before
performing the division on it.

Thus, we get statements that look like

    (a - (a % b)) / b

And probably more parentheses surrounding it too, since it would be
copy-pasted into yet another larger expression, because of course it
would.

`(a % b)` here is subtracting the remainder of `a` divided by `b`, using
the modulo operator, before it gets divided by `b`. Thus, the number
will always be divisible by `b`, so dividing it will mathematically not
produce a decimal number.

Needless to say, this is unnecessary, and very unreadable. In fact, when
I saw these for the first time, I thought they were overcomplicated
_modulos_, _not_ integer division! In C and C++, dividing an integer by
an integer will always result in an integer, so there's no need to do
all this runaround just to divide two integers.

To find all of these, I used the command

    rg --pcre2 '(.+?).+?-.+?(?=\1).+?%.+?([\d]+?).+?\/.+?(?=\2)'

which basically matches expressions of the form 'a - a % b / b', where
'a' and 'b' are identical and there could be any characters in the
spaces.
2021-09-24 17:39:31 -07:00
Misa
3fcab3a395 Remove splitseconds lookup table and inline it
There's really no reason for this simple multiplication plus division to
be in a lookup table. The compiler will optimize it faster than putting
it in a lookup table will, I'm sure.
2021-09-24 16:03:14 -07:00
Misa
c94d04a932 Use SDL_isxdigit() in favor of VVV_isxdigit()
SDL_isxdigit() was added in SDL 2.0.16, so we no longer have to
implement isxdigit() ourselves.
2021-09-23 12:26:50 -07:00
Misa
c64fd89325 Untabify every single file
YOLO.

This is a repeat of #642. As before, I just did

    rg -l '\t' | xargs -n 1 sed -i -e 's/\t/    /g'

inside the desktop_version/ folder.
2021-09-06 18:56:39 -07:00
Misa
153a5c4c3a Factor out "between" calculation to macro and func
FILESYSTEM_mountAssets() has a big comment describing the magic numbers
needed to grab FILENAME from a string that looks like
"levels/FILENAME.vvvvvv".

Instead of doing that (and having to write a comment every time the
similar happens), I've written a macro (and helper function) instead
that does the same thing, but clearly conveys the intent.

I mean, just look at the diff. Using VVV_between() is much better than
having to read that comment, and the corresponding SDL_strlcpy().
2021-05-25 15:23:34 -04:00
Misa
5133d58777 Add VVV_fillstring()
This function is simple - it takes a given buffer and its size, fills it
with a certain character, and null-terminates it. It's meant to be used
with freshly-created buffers, so we don't copy-paste code.
2021-04-11 20:56:16 -04:00
Misa
5d4c1b7e9d Refactor endsWith() to not use the STL
There's not really any reason for this function to use heap-allocated
strings. So I've refactored it to not do that.

I would've used SDL_strrstr(), if it existed. It does not appear to
exist. But that's okay.
2021-02-27 01:40:05 -05:00
Misa
6a3a1fe147
Explicitly declare void for all void parameter functions (#628)
Apparently in C, if you have `void test();`, it's completely okay to do
`test(2);`. The function will take in the argument, but just discard it
and throw it away. It's like a trash can, and a rude one at that. If you
declare it like `void test(void);`, this is prevented.

This is not a problem in C++ - doing `void test();` and `test(2);` is
guaranteed to result in a compile error (this also means that right now,
at least in all `.cpp` files, nobody is ever calling a void parameter
function with arguments and having their arguments be thrown away).
However, we may not be using C++ in the future, so I just want to lay
down the precedent that if a function takes in no arguments, you must
explicitly declare it as such.

I would've added `-Wstrict-prototypes`, but it produces an annoying
warning message saying it doesn't work in C++ mode if you're compiling
in C++ mode. So it can be added later.
2021-02-25 17:23:59 -05:00
Misa
cd5408e396 Fix potential out-of-bounds in next_split_s() wrt buffer length
next_split_s() could potentially commit out-of-bounds indexing if the
amount of source data was bigger than the destination data.

This is because the size of the source data passed in includes the null
terminator, so if 1 byte is not subtracted from it, then after it passes
through the VVV_min(), it will index 1 past the end of the passed buffer
when null-terminating it.

In contrast, the other argument of the VVV_min() does not need 1
subtracted from it, because that length does not include a null
terminator (next_split() returns the length of the substring, after all;
not the length of the substring plus 1).

(The VVV_min() here also shortens the range of values to the size of an
int, but we'll probably make size_t versions anyway; plus who really
cares about supporting massively-sized buffers bigger than 2 billion
bytes in length? That just doesn't make sense.)
2021-02-19 17:17:24 -05:00
Misa
b243b116c9 Fix VVV_isxdigit() accepting A-Za-z instead of A-Fa-f
This function accepted more characters than it should have. (I made the
same mistake on my PR to SDL, embarassingly enough...)
2021-02-15 23:50:08 -05:00
Misa
3225be6d9e Fix is_number() accepting "-" as a number
The function would accept the string "-" as a number, when it isn't one.

To fix this, don't put it as part of the loop, just add another special
case.
2021-02-15 23:50:08 -05:00
Misa
28caa994e9 Add a blank line to the end of is_number()
This is to be consistent with is_positive_num().
2021-02-15 23:50:08 -05:00
Misa
4a853fec97 Add a blank line to the end of is_positive_num()
The return statement is too close to the block above it, so I want to
space it out for aesthetics.
2021-02-15 23:50:08 -05:00
Misa
30b0fc828f Change is_number() increment to pre-increment
This is also to be consistent with is_positive_num().
2021-02-15 23:50:08 -05:00
Misa
62533300b0 Change is_number() iterator type to size_t
This is to be consistent with is_positive_num().
2021-02-15 23:50:08 -05:00
Misa
7ea70ad1d6 Fix is_number() returning true for empty strings
Just like is_positive_num(), an empty string is not a number.

I've also decided to unroll iteration 0 of the loop here so readability
is improved; this happens to also knock out the whole "accepting empty
string" thing, too.
2021-02-15 23:50:08 -05:00
Misa
b9bf2be436 Fix is_positive_num() returning true for empty strings
To account for empty strings, we simply have to special-case them.
Simple as that.

This was also a problem with the previous std::string implementation of
this function; regardless, this is fixed now.
2021-02-15 23:50:08 -05:00
Misa
89bddf98b6 Refactor is_positive_num() to use C-strings
This makes the future move to C easier, as well as just shows that it's
plain unnecessary to use an std::string here.
2021-02-15 23:50:08 -05:00
Misa
825dbac228 Make const the 'hex' argument of is_positive_num()
Always good to get in the habit of clearly marking arguments you won't
mutate as const. Makes the code easier to read.
2021-02-15 23:50:08 -05:00
Misa
62808ca97a Remove unnecessary static_cast in is_number()
Just like is_positive_num(), the implicit conversion here is completely
okay, and adding an explicit cast here introduces noise.
2021-02-15 23:50:08 -05:00
Misa
623f4edc6e Remove unnecessary static_casts in is_positive_num()
The implicit conversion is completely okay. Adding an explicit cast here
just creates noise for the reader to filter out.
2021-02-15 23:50:08 -05:00
Misa
84ac4a40c1 Refactor loading arrays from XML to not use the STL
The current way "arrays" from XML files are loaded (before this commit
is applied) goes something like this:

1. Read the buffer of the contents of the tag using TinyXML-2.

2. Allocate a buffer on the heap of the same size, and copy the
   existing buffer to it. (This is what the statement `std::string
   TextString = pText;` does.)

3. For each delimiter in the heap-allocated buffer...

   a. Allocate another buffer on the heap, and copy the characters from
      the previous delimiter to the delimiter you just hit.

   b. Then allocate the buffer AGAIN, to copy it into an std::vector.

4. Then re-allocate every single buffer YET AGAIN, because you need to
   make a copy of the std::vector in split() to return it to the caller.

As you can see, the existing way uses a lot of memory allocations and
data marshalling, just to split some text.

The problem here is mostly making a temporary std::vector of split text,
before doing any actual useful work (most likely, putting it into an
array or ANOTHER std::vector - if the latter, then that's yet another
memory allocation on top of the memory allocation you already did; this
memory allocation is unavoidable, unlike the ones mentioned earlier,
which should be removed).

So I noticed that since we're iterating over the entire string once
(just to shove its contents into a temporary std::vector), and then
basically iterating over it again - why can't the whole thing just be
more immediate, and just be iterated over once?

So that's what I've done here. I've axed the split() function (both of
them, actually), and made next_split() and next_split_s().

next_split() will take an existing string and a starting index, and it
will find the next occurrence of the given delimiter in the string. Once
it does so, it will return the length from the previous starting index,
and modify your starting index as well. The price for immediateness is
that you're supposed to handle keeping the index of the previous
starting index around in order to be able to use the function; updating
it after each iteration is also your responsibility.

(By the way, next_split() doesn't use SDL_strchr(), because we can't get
the length of the substring for the last substring. We could handle this
special case specifically, but it'd be uglier; it also introduces
iterating over the last substring twice, when we only need to do it
once.)

next_split_s() does the same thing as next_split(), except it will copy
the resulting substring into a buffer that you provide (along with its
size). Useful if you don't particularly care about the length of the
substring.

All callers have been updated accordingly. This new system does not make
ANY heap allocations at all; at worst, it allocates a temporary buffer
on the stack, but that's only if you use next_split_s(); plus, it'd be a
fixed-size buffer, and stack allocations are negligible anyway.

This improves performance when loading any sort of XML file, especially
loading custom levels - which, on my system at least, I can noticeably
tell (there's less of a freeze when I load in to a custom level with
lots of scripts). It also decreases memory usage, because the heap isn't
being used just to iterate over some delimiters when XML files are
loaded.
2021-02-15 23:24:31 -05:00
Misa
5de7c180ea Give static storage duration to radix in ss_toi()
Since the value is a const value that reads from an integer literal, it
should be static so we read it directly from storage instead of copying
it.
2021-02-08 09:44:29 -05:00
Misa
09841ef3a5 Don't mutate the decimal place in ss_toi()
This fixes a bug where "12" gets properly evaluated as 12, but "148"
gets evaluated as 1408. It's because `place` gets multiplied by `radix`
again, so `retval` gets multipled by 100 instead of 10.

There's no reason to have a `place` variable, so I've removed it
entirely. This simplifies the function a little bit.
2021-02-08 09:44:29 -05:00
Misa
06cc5afe27 Pass input of UtilityClass::GCString() by const reference
This avoids an unnecessary copy of the input std::vector, since we don't
need to modify it for anything. This cuts down on unnecessary memory
operations.
2021-02-07 18:30:13 -05:00
Misa
46b0257cf1 Refactor ss_toi() to not use stringstreams
Apart from the std::string, this function no longer uses the STL.

ss_toi() is a simple function - it converts the input into an int,
taking as many digits as possible until it reaches a non-digit
character, at which point it stops. It's trivial to implement this
without the STL.

I could've used Int() here, but that would've required copying the
string to a temporary buffer to insert a null-terminator (we can't just
use a pointer-and-length data type either, the string functions don't
operate like that - one disadvantage of C strings!). Instead, I decided
to implement my own conversion to int here, because I don't think the
way we humans write our Arabic numerals is going to change anytime soon.

Also, the std::string input is now passed by const reference, instead of
making a copy - cutting down on unnecessary memory operations.
2021-02-07 18:30:13 -05:00
Misa
e1ae25b29b Use case-switch inside GCChar() instead of if-else tree
A case-switch here takes up less lines (at least with next-line brace
style) and doesn't repeat the 'button ==' part over and over.
2021-02-07 18:30:13 -05:00
Misa
eea37046ef Add constness to GCChar() input and switch position of asterisk
I personally like putting the asterisk with the type, because despite
the language parsing the asterisk as a part of the name, the pointer
part is clearly a part of the return type of the function. Also,
put constness here, to indicate that the input won't be modified inside
the function.
2021-02-07 18:30:13 -05:00
Misa
3cd18b337a Remove comment from GCChar()
This comment indicates that the function is used by
UtilityClass::GCString(). Which is unnecessary, because the reader can
trivially search for usages of GCChar in the file itself (the 'static'
preceding the function should be a good enough hint) - and if there
aren't any, then the reader will know the function is unused, whereas if
they read the comment, they would have been under the assumption that it
wasn't used. (There might also a compiler warning about it being unused,
which would be more confusing if the comment was still there.)

Point is, comments can get outdated, so removing the comment here makes
the code more self-documenting.
2021-02-07 18:30:13 -05:00
Misa
f9fa56a08a Remove unused cctype #include from UtilityClass.cpp
This header used to be needed for isxdigit(), but ever since we switched
to using our own VVV_isxdigit(), we don't need it anymore.
2021-02-07 18:30:13 -05:00
Misa
769f99f590 Reduce dependency on libc functions
During 2.3 development, there's been a gradual shift to using SDL stdlib
functions instead of libc functions, but there are still some libc
functions (or the same libc function but from the STL) in the code.

Well, this patch replaces all the rest of them in one fell swoop.

SDL's stdlib can replace most of these, but its SDL_min() and SDL_max()
are inadequate - they aren't really functions, they're more like macros
with a nasty penchant for double-evaluation. So I just made my own
VVV_min() and VVV_max() functions and placed them in Maths.h instead,
then replaced all the previous usages of min(), max(), std::min(),
std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max().

Additionally, there's no SDL_isxdigit(), so I just implemented my own
VVV_isxdigit().

SDL has SDL_malloc() and SDL_free(), but they have some refcounting
built in to them, so in order to use them with LodePNG, I have to
replace the malloc() and free() that LodePNG uses. Which isn't too hard,
I did it in a new file called ThirdPartyDeps.c, and LodePNG is now
compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition.

Lastly, I also refactored the awful strcpy() and strcat() usages in
PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save
migration is getting axed in 2.4, but it still bothers me to have
something like that in the codebase otherwise.

Without further ado, here is the full list of functions that the
codebase now uses:

- SDL_strlcpy() instead of strcpy()
- SDL_strlcat() instead of strcat()
- SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above)
- VVV_min() instead of min(), std::min(), or SDL_min()
- VVV_max() instead of max(), std::max(), or SDL_max()
- VVV_isxdigit() instead of isxdigit()
- SDL_strcmp() instead of strcmp()
- SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi()
- SDL_strstr() instead of strstr()
- SDL_strlen() instead of strlen()
- SDL_sscanf() instead of sscanf()
- SDL_getenv() instead of getenv()
- SDL_malloc() instead of malloc() (replacing in LodePNG as well)
- SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 14:02:31 -05:00
Misa
e9c62ea9a3 Clean up unnecessary exports and add static keywords
This patch cleans up unnecessary exports from header files (there were
only a few), as well as adds the static keyword to all symbols that
aren't exported and are specific to a file. This helps the linker out in
not doing any unnecessary work, speeding it up and avoiding silent
symbol conflicts (otherwise two symbols with the same name (and
type/signature in C++) would quietly resolve as okay by the linker).
2021-01-10 12:23:59 -05:00
Misa
cbceeccf78 Clean up and prevent unnecessary qualifiers to self
By "unnecessary qualifiers to self", I mean something like using the
'game.' qualifier for a variable on the Game class when you're inside a
function on the Game class itself. This patch is to enforce consistency
as most of the code doesn't have these unnecessary qualifiers.

To prevent further unnecessary qualifiers to self, I made it so the
extern in each header file can be omitted by using a define. That way,
if someone writes something referring to 'game.' on a Game function,
there will be a compile error.

However, if you really need to have a reference to the global name, and
you're within the same .cpp file as the implementation of that object,
you can just do the extern at the function-level. A good example of this
is editorinput()/editorrender()/editorlogic() in editor.cpp. In my
opinion, they should probably be split off into their own separate file
because editor.cpp is getting way too big, but this will do for now.
2020-09-28 01:34:40 -04:00
Misa
25f27d502a Prevent undefined behavior with integer under/overflow with help.Int()
It's possible that SDL_atoi() could call the libc atoi(), and if a
string is provided that's too large to fit into an integer, then that
would result in undefined behavior. To avoid this, use SDL_strtol()
instead.
2020-09-27 17:05:00 -04:00
Misa
d1938a151f Add fallback parameter to UtilityClass::Int()
If necessary, the caller can provide a fallback to be returned in case
the input given isn't a valid integer, instead of having to duplicate
the is_number() check.
2020-08-07 01:00:49 -04:00
Misa
037065910f Add UtilityClass::Int()
This is simply a wrapper function around SDL_atoi(), because SDL_atoi()
could call libc atoi(), and whether or not invalid input passed into the
libc atoi() is undefined behavior depends on the given libc. So it's
safer to just add a wrapper function that checks that the string given
isn't bogus.
2020-08-07 01:00:49 -04:00
Misa
502d597aeb Add is_number()
This will be used to check if a string is a valid integer. So negative
numbers are accepted, unlike is_positive_num().
2020-08-07 01:00:49 -04:00
Misa
b8b616e282 Replace std::is[x]digit() with non-STL versions
std::isdigit() can be replaced with SDL_isdigit(), but there's no
equivalent for std::isxdigit() in the SDL standard library. So we'll
just use libc isxdigit() instead, it's fine.
2020-08-07 01:00:49 -04:00
Misa
52f7a587fe Separate includes into sections and alphabetize them
Okay, so basically here's the include layout that this game now
consistently uses:

[The "main" header file, if any (e.g. Graphics.h for Graphics.cpp)]
[blank line]
[All system includes, such as tinyxml2/physfs/utfcpp/SDL]
[blank line]
[All project includes, such as Game.h/Entity.h/etc.]

And if applicable, another blank line, and then some special-case
include screwy stuff (take a look at editor.cpp or FileSystemUtils.cpp,
for example, they have ifdefs and defines with their includes).
2020-07-19 21:37:40 -04:00
Misa
2af396fb30 Consistently use angle brackets for SDL.h includes
That's how it should be done, because the SDL headers aren't going to be
installed in this repository. The game was a bit inconsistent before but
now it isn't anymore.
2020-07-19 21:37:40 -04:00
Misa
78181bc676 Statically allocate strings in UtilityClass::number()
It slightly bothered me that these weren't statically allocated, so they
are now.
2020-07-06 11:19:24 -04:00
Misa
5f91bdd073 Turn splitseconds into an array
It's always 30, there's no need for it to be a vector.
2020-07-06 11:19:24 -04:00
Misa
118008d824 Move temp/temp2 off of UtilityClass, remove globaltemp
There's no reason for the temp variables to be on the class itself.
2020-07-06 11:19:24 -04:00
Misa
34e89bfcd3 Move endsWith() to UtilityClass.cpp and put it in header file
This ensures that endsWith() can be used outside of editor.cpp.

When leo60228 originally wrote endsWith(), it was static, but I asked
him on Discord just now and he more-or-less confirmed that it's fine if
it's not static. If it was static, it would be confined to
UtilityClass.cpp now instead!
2020-06-21 20:25:22 -04:00
leo60228
a99e976402 Support hex entities in metadata 2020-06-15 20:32:10 -04:00
Misa
884035fd2e Add unsigned char static_cast to argument of std::isdigit()
Apparently it results in Undefined Behavior if the argument given isn't
representable as an unsigned char. This means that (potentially) plain
char and signed chars could be unsafe to use as well.

It's rare that this could happen in practice, though. std::isdigit() is
only used by is_positive_num() which is only used by find_tag(), so
someone would have to deliberately put something crazy after an `&#` in
a custom level file in order for this to happen. Still, better to be
safe than sorry and all.
2020-06-13 01:25:17 -04:00
Misa
fdb44cc209 Add <cctype> include
This fixes a compile error that could happen where the compiler doesn't
know what std::isdigit() is, but I'm puzzled as to why this wasn't
happening earlier, 'cause I've only been reported that it happens by
only one person.
2020-06-13 01:25:17 -04:00