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

2257 commits

Author SHA1 Message Date
Misa
d45e6f6254 Remove game.gametimer in favor of game.frames
PR #279 added game.gametimer solely for the editor ghosts feature. It
seems that whoever originally wrote it (Leo for the now-dead VVVVVV:
Community Edition, I believe) forgot that the game already had its own
timer, that they could use.

The game timer does increment on unfocus pause (whereas this doesn't),
but that's a separate issue, and it ought to not do that.
2021-03-21 20:53:11 -04:00
Misa
17169320b4 Fix text box deltaframe flashing on deltaframes after fully opaque
So #434 didn't end up solving the deltaframe flashing fully, only
reduced the chances that it could happen.

I've had the Level Complete image flash a few times when the Game Saved
text box pops up. This seems to be because the Level Complete image is
based off of the text box being at y-position 12, and the Game Saved
text box is also at y-position 12. Level Complete only gets drawn if the
text box additionally has a red channel value of 165, and the Game Saved
text box has a red channel value of 174. However, there is a check that
the text box be fully opaque first before drawing special images. So
what went wrong?

Well, after thinking about it for a while, I realized that even though
there is indeed an opaqueness check, the alpha of the text box updates
BEFORE it gets drawn. And during the deltaframes immediately after it
gets updated, the text box is considered fully opaque. It's completely
possible for the linear interpolation to end up with a red channel value
of 165 during these deltaframes, while the text box is opaque as well.

As always, it helps if you have a high refresh rate, and run the game
under 40% slowdown.

Anyways, so what's the final fix for this issue? Well, use the text box
'target' RGB values instead - its tr/tg/tb attributes instead of its
r/g/b attributes. They are not subject to interpolation and so are
completely reliable. The opaqueness check should still be kept, though,
because the target values don't account for opaqueness. And this way, we
get no more deltaframe flashes during text box fades.

An even better fix would be to not use magic RGB values to draw special
images... but that'd be something to do later.
2021-03-21 19:01:36 -04:00
Ethan Lee
415d4790e2
Fix a crash on first unfocus 2021-03-21 17:19:08 -04:00
Ethan Lee
7abd4bb8d8
Visual Studio buildfix 2021-03-21 17:15:36 -04:00
Misa
4a79f02842 Add braces around sub-object in unfocused_func_list
Clang warns on this. This doesn't fix anything but it does ensure that
whoever's reading it won't be focused as to whether or not omitting the
second set of braces is legal or not.
2021-03-21 17:03:17 -04:00
Misa
02560ca6e5 Remove now-unneeded kludge for finalmode entity colors
Previously, with the wrong loop order, this kludge needed to exist so
entities in finalmode didn't have wrong colors for 1 frame when entering
a room. But now the loop order has been fixed, and so this kludge is no
longer needed.
2021-03-21 02:55:42 -04:00
Misa
287061c768 Fix filter/screenshake/flash update order
In 2.2, at render time, the game rendered screenshakes and flashes if
their timers were above 0, and then decremented them afterwards. The
game would also update the analogue filter right before rendering it,
too.

In 2.3, this was changed so the flash and screenshake timers were
unified, and also done at the end of the frame - right before rendering
happened. This resulted in 1-frame flashes and screenshakes not
rendering at all. The other changes in this patchset don't fix this
either. The analogue filter was also in the wrong order, but that is
less of an issue than flashes and screenshakes.

So, what I've done is made the flash and screenshake timers update right
before the loop switches over to rendering, and only decrements them
when we switch back to fixed functions (after rendering). The analogue
filter is also updated right before rendering as well. This restores
1-frame flashes and screenshakes, as well as restores the correct order
of analogue filter updates.
2021-03-21 02:55:42 -04:00
Misa
094209bd12 Move logical onground/onroof updates to start of gamelogic
This reintroduces 2-frame edge-flipping after the 1-frame input delay
got removed. This is because along with processing input and moving
Viridian, logical onground/onroof assignments need to processed in the
same between-render sequence as well - otherwise Viridian only gets 1
frame of edge-flipping due to frame ordering.
2021-03-21 02:55:42 -04:00
Misa
63a60b11cc Split onground/onroof into visual and logical variables
I will need to separate these into two different variables because I
will need to move logical onground/onroof assignments to the start of
gamelogic() - if I kept them together, however, that would change the
visuals of onground/onroof, which I want to keep consistent with 2.2.
2021-03-21 02:55:42 -04:00
Misa
d5d9d9ba96 Re-remove 1-frame input delay
To do this, GAMEMODE input needs to be processed, and Viridian needs to
be moved, in the same sequence between render frames. So just move
gameinput to after gamerender. Yes, this is not 2.2 order, but gameinput
only handles player input and nothing else - plus a 1-frame input delay
feels really awful to play with in over-30-mode.
2021-03-21 02:55:42 -04:00
Misa
f1f434accc Move key.Poll() calls to just before input funcs
In order to re-remove the 1-frame input delay, we will have to poll
input right after rendering a frame - in other words, just before an
input function gets called.

To do this, I've added a new function enum type - Func_input - that is
the same as a fixed function, but before its function gets called,
key.Poll() gets called. And all input functions have been updated to use
this enum accordingly.
2021-03-21 02:55:42 -04:00
Misa
ab26985fde Re-fix crewmate directions (without copy-pasting)
This once again fixes the facing directions of crewmates upon room load,
except now it covers more cases.

So, here is the saga so far:

- 2.0 (presumably) to 2.2: crewmate direction fix is special-cased at
  the end of mapclass::loadlevel(). Only covers crewmates created during
  the room load, does not cover crewmates created from scripts, only
  covers state 18 of crewmates.

- 2.3 currently (after #220): crewmate direction fix is moved to
  entityclass::createentity(), which covers every avenue of crewmate
  creation (including from scripts), but still only covers state 18.

- This commit: crewmate direction fix now covers every possible state of
  the crewmate, also does not copy-paste any code.

What I've done instead is to make it so createentity() will immediately
call updateentities() on the pushed-back entity. This is kludge-y, but
is completely okay to do, because unlike other entities, crewmate
entities never change their state or have any side-effects from
double-evaluation, meaning calling updateentities() on them is
idempotent and it's okay to call their updateentities() more than once.

This does have the slight danger that if the states of crewmates were to
change in the future to no longer be idempotent, this would end up
resulting in a somewhat hard-to-track-down double-evaluation bug, but
it's worth taking that risk.

This fix is not applied to entity 14 (the supercrewmate) because it is
possible that calling updateentities() on it will immediately remove the
entity, which is not idempotent (it's changing the state of something
outside the object). Supercrewmates are a bit difficult to work with
outside of the main game anyways, and if you spawn them you could
probably just use the changedir() script command to fix their direction,
so I'm not inclined to fix this for them anyway.
2021-03-21 02:55:42 -04:00
Misa
cb5d181ce8 Remove entityclass::createentity() deltaframe kludge
This copy-pasted code only existed because the previous loop order was
incorrect and rendered entities before they would get properly updated
by the fixed render function. Now, the fixed render function is
guaranteed to be called before the render function, so we can rely on
that to update the drawframe and realcol of entities instead of
duplicating the code ourselves in createentity().

The drawframe assignment is still kept to fix the case where dying while
completestop is active (i.e. during a trinket or crewmate rescue
cutscene) and respawning in a different room won't turn everything into
Viridian sprites.
2021-03-21 02:55:42 -04:00
Misa
2d9d0cffa5 Fix 1-frame glitch when going to in-game options from edsettings
The menu creation of Graphic Options or Game Options, as well as the
map.nexttowercolour() call, all need to be deferred until the end of the
frame.
2021-03-21 02:55:42 -04:00
Misa
951679b1f8 Fix 1-frame background glitch when returning from in-game options
The background would change for 1 frame before sending you back to the
pause menu or editor settings. The map.nexttowercolour() call needs to
be deferred until the end of the frame.
2021-03-21 02:55:42 -04:00
Misa
5088ff40e9 Fix 1-frame text glitch when returning to editor settings from options
The returnmenu() needs to be deferred until the end of the frame.
2021-03-21 02:55:42 -04:00
Misa
8a3e292041 Fix 1-frame text glitch returning to pause menu from in-game options
The new loop order introduces a glitch where the menu would display
whichever menu was saved to kludge_ingametemp for 1 frame right as the
user returned to the pause menu. This happened because the
game.returntomenu() happens in titleinput(), which comes before
titlerender(). To fix this, we just need to defer it to the end of the
frame.
2021-03-21 02:55:42 -04:00
Misa
3da0e31215 Remove game.shouldreturntoeditor in favor of using defer callback
game.shouldreturntoeditor was added to fix a frame ordering issue that
was causing a bug where if you started playtesting in a room with a
horizontal/vertical warp background, and exited playtesting in a
different room that also had a horizontal/vertical warp background and
which was different, then the background of the room you exited in would
slowly scroll offscreen, when you re-entered the editor, instead of the
background consisting entirely of the actual background of the room.

Namely, the issue was that the game would render one more frame of
GAMEMODE after graphics.backgrounddrawn got set to false, and re-set it
to true, thus negating the background redraw, so the editor background
would be incorrect.

With defer callbacks, we can now just use a couple lines of code,
instead of having to add an extra kludge variable and putting handling
for it all over the code.
2021-03-21 02:55:42 -04:00
Misa
32be2fcd81 Update player lerpoldxp/yp in moveplayer()
Just like gotoposition(), the player would otherwise appear to "zip"
after the command got run. This did not happen in the previous loop
order.
2021-03-21 02:55:42 -04:00
Misa
c8537beac1 Add deferred callbacks to game loop
Sometimes, there needs to be code that gets ran at the end of the game
loop, otherwise rendering issues might occur. Currently, we do this by
special-casing each deferred routine (e.g. shouldreturntoeditor), but it
would be better if we could generalize this deference instead.

Deferred callbacks can be added using the DEFER_CALLBACK macro. It takes
in one argument, which is the name of a function, and that function must
be a void function that takes in no arguments. Also, due to annoying C++
quirks, void functions taking no arguments cannot be attributes of
objects (because they have an implicit `this` parameter), so it's
recommended to create each callback separately before using the
DEFER_CALLBACK macro.
2021-03-21 02:55:42 -04:00
Misa
c8958de537 Update player lerpoldxp/yp in gotoposition()
Otherwise, the player would appear to "zip" during the deltaframes
between their previous position and their new position. This did not
happen in the previous game loop order and only happens in the new one.
2021-03-21 02:55:42 -04:00
Misa
af70076088 Remove now-unneeded deltaframe fix when going to in-game settings
Previously, before the game loop order got fixed, going to the in-game
settings would switch over to the new render function too early, causing
a deltaframe glitch that had to be fixed. But now, the render function
only gets switched when the current gamestate's function list gets
finished executing, so the game won't suddenly switch to titlerender()
in the middle of the ACTION press to the in-game settings screen.

As a consequence, titleupdatetextcol() no longer needs to be exported to
Input.cpp.
2021-03-21 02:55:42 -04:00
Misa
c26b701f5b Remove gravity line kludge from Graphics::drawgravityline()
Now that the game loop order is now fixed, there is no longer any need
for this kludge.
2021-03-21 02:55:42 -04:00
Misa
5e2fc6f0fe Move updating lerpoldxp/yp to start of gamelogic()
The previous location of this loop was placed there because it happened
just after the end of the render function. Now that the loop order is
fixed, the first thing that happens after the render function is the
start of gamelogic(), so this loop should go there now, else entity
positions won't be interpolated.

Also it now preincrements instead of postincrements because I like
preincrements.
2021-03-21 02:55:42 -04:00
Misa
585ae47d78 Remove script.dontrunnextframe kludge
Now that the game loop order is fixed, this kludge (on top of kludge) is
no longer needed, and can be safely removed.
2021-03-21 02:55:42 -04:00
Misa
c82c2afbbd Unindent unfocused_run() and focused_begin() from previous commit
As always, indentation changes are applied in a separate commit to
minimize diff noise.
2021-03-21 02:55:42 -04:00
Misa
1e9fb6aac0 Generalize game loop order and fix it to what it was in 2.2
Okay, so the reason why all render functions were moved to the end of
the frame in #220 is because it's simpler to call two fixed functions
and then a delta function instead of one fixed function, then a delta
function, and then another fixed function.

This is because fixed functions need special handling inside
deltaloop(), and you can't simply duplicate this handling after calling
a delta function. Oh, and to make matters worse, it's not always
fixed-delta-fixed, sometimes (like in MAPMODE and TELEPORTERMODE) it's
delta-fixed-fixed, so we'd need to handle that somehow too.

The solution here is to generalize the game loop and factor out each
function, instead of hardcoding it. Instead of having hardcoded
case-switches directly in the loop, I made a function that returns an
array of functions for a given gamestate, along with the number of
functions, then the game loop processes it accordingly. In fixedloop(),
it iterates over the array and executes each function until it reaches a
delta function, at which point it stops. And when it reaches the end of
the array, it goes back to the start of the array.

But anyway, if it gets to a delta function, it'll stop the loop and
finish fixedloop(). Then deltaloop() will call the delta function. And
then on the next frame, the function index will be incremented again, so
fixedloop() will call the fixed functions again.

Actually, the previous game loop was actually made up of one big loop,
with a gamestate function loop nested inside it, flanked with code that
ran at the start and end of the "big loop". This would be easy to handle
with one loop (just include the beginning and end functions with the
gamestate functions in the array), except that the gamestate functions
could suddenly be swapped out with unfocused functions (the ones that
run when you unfocus the window) at any time (well, on frame boundaries,
since key.isActive only got checked once, guarding the entire "inner
loop" - and I made sure that changing key.isActive wouldn't immediately
apply, just like the previous game loop order) - so I had to add yet
another layer of indirection, where the gamestate functions could
immediately be swapped out with the unfocused functions (while still
running the beginning and end code, because that was how the previous
loop order worked, after all).

This also fixes a regression that the game loop that #220 introduced
had, where if the fixed functions switched the gamestate, the game would
prematurely start rendering the gamestate function of the new gamestate
in the deltaframes, which was a source of some deltaframe glitches. But
fixing this is likely to just as well cause deltaframe glitches, so it'd
be better to fix this along with fixing the loop order, and only have
one round of QA to do in the end, instead of doing one round after each
change separately.

Fixes #464... but this isn't the end of the patchset. There are bugs
that need to be fixed, and kludges that need to be reverted.
2021-03-21 02:55:42 -04:00
Misa
5e440ac48d Remove special text box checks for y-position 180
Y-position 180 would be the position of the Level Complete and Game
Complete special text boxes in Flip Mode. However, since the y-position
of flipme text boxes actually no longer change (because we have to
accomodate changing Flip Mode on-the-fly), these text boxes will never
actually be y-position 180 - so we should remove these checks for
clarity.
2021-03-21 02:53:25 -04:00
Misa
596696dcf3 Make foundtrinket() Flip Mode-aware
A-ha! I've spotted an inconsistency! The normal trinket collection text
boxes (gamestate 1000-1003) is aware of Flip Mode, and will position
themselves accordingly to read the correct way in Flip Mode. However,
foundtrinket() doesn't do this.

Well, now it does.
2021-03-21 02:53:25 -04:00
Misa
db9ee0d8e3 Switch flipme script command to use flipme textbox attribute
This is why the text box attribute was named flipme, after all.

You may have noticed that the flipme command inverts textflipme instead
of simply setting it to true. Well, that's because it should be the same
as the previous behavior, which was essentially to invert it instead of
setting it to true - i.e. calling flipme twice would keep the original
text box position in Flip Mode, which means it would be upside-down
(this is a lot of flipping to keep track of...) - because flipme added
to texty in-place instead of simply assigning to it. (It did the
calculation incorrectly in 2.2 and previous, but I digress.)

Similarly, textflipme is not reset in hardreset(), because none of the
other script text box variables are reset either.
2021-03-21 02:53:25 -04:00
Misa
6939563e6d Switch all flippable text boxes to use createtextboxflipme
This ensures that if the player decides to toggle Flip Mode while one of
these text boxes is up, they won't be oriented improperly. Additionally,
it also de-duplicates a bunch of Flip Mode check code, which is also a
win.
2021-03-21 02:53:25 -04:00
Misa
c7cc2f4adc Add createtextboxreal() and createtextboxflipme()
createtextboxreal() is the same as createtextbox(), but with a flipme
parameter added to create text boxes that have their flipme attribute
set to true. createtextbox() just calls createtextboxreal() with flipme
set to false, and createtextboxflipme() just calls createtextboxreal()
with flipme set to true; this is because I do not want to use C++
function overloading.
2021-03-21 02:53:25 -04:00
Misa
1a9f2d9342 Add flipme attribute to textboxclass
Instead of calculating the y-position of the text box when it's created,
we will store a flag that says whether or not the text box should be
flipped in Flip Mode (and thus stay right-side-up), and when it comes
time to draw the text box, we will check Flip Mode and calculate the
position then.
2021-03-21 02:53:25 -04:00
Misa
2ac13815e4 Remove textrect attribute from textboxclass
Instead of duplicating the same variables over and over again,
Graphics::drawgui() can just make its own SDL_Rect. It's not that hard.

As far as I can tell, textrect was always being properly kept up to date
by the time Graphics::drawgui() got around to rendering
(textboxclass::resize() keeps being called a LOT), so this shouldn't be
a noticeable change from the user perspective.
2021-03-21 02:53:25 -04:00
Misa
334302c800 Remove unused x/y textboxclass attributes
These unused variables distract from properly analyzing the code when
you read it, since the xp/yp attributes of textboxclass already exist,
too.
2021-03-21 02:53:25 -04:00
Misa
30719b87db De-duplicate "Game Saved" telesave textbox
The "Game Saved" text box, along with its associated telesave() call,
exists in both Game.cpp and Script.cpp, so one of them is the copy-paste
of the other. Unfortunately this copy-paste resulted in an inconsistency
where both of them don't check for the same things when deciding whether
or not the telesave should actually happen (this is why you don't
copy-paste, kids... it's scary!).

Either way, de-duplicating this now is less work for me later.
2021-03-21 02:53:25 -04:00
Misa
5de884f584 De-duplicate Level Complete sequence textboxes
Every Level Complete sequence is the same copy-pasted thing, but with
minor changes. To make my work easier, I'm de-duplicating them so I have
less text boxes to change later, and less grind to grind.
2021-03-21 02:53:25 -04:00
Misa
b7ca408076 Remove default arguments from createtextbox()
These default arguments are never used anywhere. And if they were used
anywhere, it'd be better to explicitly say 255,255,255 than make readers
have to look at the header file to see what these default to. Also, this
creates four different overloads of createtextbox(), instead of only
two - but we ought to not be using function overloading anyway.
2021-03-21 02:53:25 -04:00
Misa
d5ed49d8dc Remove commented-out code from Game.cpp and Script.cpp
These commented-out code blocks just get in the way of clarity when I'm
refactoring flipped textboxes created in the gamestate system. So I'm
getting rid of them. If we need them back, we always have Git history.
2021-03-21 02:53:25 -04:00
Misa
5f2b9409b2 De-duplicate Gravitron initial message
Since the only difference is the y-positions, I've decided to remove the
copy-pasted code. A better solution would be to have a function that
draws multiline text and handles it accordingly in Flip Mode, but that
could be done later.
2021-03-21 02:06:39 -04:00
Misa
c7e807541c De-duplicate Flip Mode textbox crewmate rendering
The only difference between Flip Mode and normal mode is the y-position
and sprite used to draw the crewmates. Everything else is the same, so
I've removed the copy-pasted portion.

The diff might look a bit ugly due to the unindentation.
2021-03-21 02:06:39 -04:00
Misa
52a7d42672 De-duplicate Flip Mode text printing
Since the only difference in Flip Mode is the positiveness/negativeness
of the iterator variable, plus the starting y-offset, I've removed the
copy-pasted code and did this instead.

The diff might look a bit ugly due to the unindentation.
2021-03-21 02:06:39 -04:00
Misa
f6ecf83190 Ensure oldfadeamount is updated when fadeamount is
Like cutscene bars, I've added Graphics::setfade(), to ensure that no
deltaframe rendering glitches happen due to oldfadeamount not being
updated properly.

And indeed, this fixes a deltaframe rendering glitch that happens if you
return to the editor from playtesting on a faded-out screen, then fade
out again (by either re-entering playtesting and then cause a fadeout to
happen again, or by quitting from the editor afterwards). The same
glitch also happens outside of in-editor playtesting if you exit to the
menu while the screen is faded out.
2021-03-21 01:06:29 -04:00
Misa
f22756dd99 Ensure oldcutscenebars is updated when cutscenebarspos is
To do this, I've added Graphics::setbars(), to make sure
oldcutscenebarspos always gets assigned when cutscenebarspos is. This
fixes potential deltaframe rendering issues if these two mismatch.
2021-03-21 01:06:29 -04:00
Misa
9e2716b253 Fix a few missing implicit void arg declarations
While working on #535, I noticed that editormenuactionpress() still
didn't do the explicit void declaration. Then I ran `rg 'void.*\(\)'`
and found three other functions that I somehow missed in #628. Whoops.
Well, now they no longer are missed.
2021-03-19 10:27:54 -04:00
Misa
2c8d338e47 Add graphic options and game options to editor settings
This is a small quality-of-life tweak that makes it so if you're in the
middle of editing a level, you don't have to save the level, exit to the
menu, change whatever setting you wanted, re-enter the editor, and type
in the level name, just to change one setting. This is the same as
adding Graphic Options and Game Options to the in-game pause menu,
except for the editor, too.

To do this, I'm reusing Game::returntopausemenu() (because all of its
callers are the same callers for returning to editor settings) and
renamed it to returntoingame(), then added a variable named
ingame_editormode to Game. When we're in the options menus but still in
the editor, BOTH ingame_titlemode and ingame_editormode will be true.
2021-03-18 23:01:00 -04:00
Misa
fc8c7d034d Add being able to press Esc to return to previous menu
This is a small quality-of-life thing that makes it so you don't have to
move your menu selection all the way over to the "return" button in
order to return to the previous menu. You can just press Escape instead
to return to the previous menu. The previous behavior of pressing Escape
was to bring up the 'confirm quit' menu, or if you were in an options
menu in-game, return to the pause menu.

If you're on the main menu (and thus don't have any previous menu) and
press Escape, the game will instead bring up the 'confirm quit' menu.
For consistency, the "quit game" option on the main menu will also bring
up the 'confirm quit' menu as well, instead of immediately closing the
game.

Pressing the controller button mapped to Escape will also work as well.

The only menus that don't have return buttons are the 'countdown' menus
- so the game will not let you press Escape if there's a menu countdown
happening.

Now that pressing Escape in the 'continue' menu will just bring you back
to the 'play' menu, there's no need to specifically put
map.nexttowercolour() first when canceling the 'confirm quit' menu.
2021-03-18 20:38:23 -04:00
Misa
61e5b819e4 Fix VVVVVV-Man not being interpolated
This is because it directly uses the xp and yp of the player instead of
the interpolated xp and yp. Whoops.
2021-03-18 18:00:45 -04:00
Misa
e70586b154 Inline cutscene bars timer for gamemodes that used it in 2.2
As part of my work in #535, I've noticed that 2.3 currently with 2.2
loop order doesn't have interpolated cutscene bars. This is because
cutscene bars in 2.3 get updated at the start of the frame, which
interpolates them correctly until the render functions are put in their
proper place.

There is, however, a somewhat bigger issue, outside the scope of #535,
where cutscene bars always get updated regardless of which gamemode you
are in. Previously in 2.2 and previous, cutscene bars only got updated
in GAMEMODE and TELEPORTERMODE; sometime during 2.3, the cutscene bars
timer got pulled out of all the individual game modes and moved to the
very start of the loop. (I was probably the one who did this change;
I've been caught in a trap of my own devising.)

Thus, going to MAPMODE during the cutscene bars animation doesn't keep
their position paused like it would in 2.2. This is also categorically a
more-than-visual change, since the untilbars() script command depends
on the cutscene bars timer. I see no reason for the cutscene bars to
behave differently in this way than 2.2; #535 would also end up doing
the same fix more-or-less anyway.

Since TELEPORTERMODE currently uses the same renderfixed function as
MAPMODE, I've had to add a teleporterrenderfixed() that just calls
maprenderfixed(), but also does the cutscene bars timer.
2021-03-17 11:03:14 -04:00
Misa
4e12c162d4 Add SDL2 version number to desktop_version/ README
As a partial fix for #618, adding the SDL2 version number to the README
will clarify that you need a specific version of SDL2 in order to
compile (and run) the current version of the game (2.3 at the time of
writing); in the future, the SDL2 dependency will be upgraded with each
SDL release.

This is to avoid error messages that complain about missing symbols like
SDL_zeroa() (added in SDL 2.0.14) not being present at the time of
compilation.

Closes #626.
2021-03-17 10:58:13 -04:00