In summary, if you got to gamestate 1002 or 1012 without an advancetext,
and you had completestop on, you were basically softlocked. So just add
those gamestates there and advance the gamestate if advancetext is off.
This infinite loop would occur because once you pressed left or right,
the game keeps searching through all the list of teleporters until it
finds one that is unlocked. But if there's none that are unlocked, then
the game goes into an infinite loop, which brings up the Not Responding
dialog on Windows so you can kill it.
Normally, you're not supposed to have no teleporters unlocked while
being able to access a teleporter, but you can achieve this by going to
Class Dismissed from a custom level (while making sure you don't start
in 0,0, because there's a teleporter there that you would unlock).
The solution is to make sure at least one teleporter is unlocked before
doing any searching.
A do-while is just a while-loop, but the inner block will always run
once before the conditional is checked.
It looks like in order to achieve this desired behavior (always run the
block once before checking the conditional), instead of using a do-while
loop, Terry just used a normal while-loop and copy-pasted the inner
block on the outside.
So I'm de-duplicating the code.
This was caused by the fact that not all unlocks were done through the
Game::unlocknum() function. Some just set the unlock number directly.
But it's fixed now.
Ved has this useful feature where instead of having to manually travel
to a room whose coordinates you know, you can just press G and type in
coordinates to go there.
VCE added this, but I changed the text to be "x,y" instead of "(x,y)"
because otherwise it could confuse someone into thinking they need to
type parentheses when in reality they don't need to and typing them will
just make it not work.
Also I made sure to add an error message if the user types in an invalid
format. Failing silently would just confuse people, and maybe they'll
start thinking the feature doesn't work or something like that. VCE
doesn't have this helpful error message.
Lastly, VCE has a bug where if you use the shortcut to go from one
horizontally/vertically warping room to another, the background of the
previous room will still be there and scroll off with the background of
the room you went to, instead of just having the new background only.
This is because they forgot a 'graphics.backgrounddrawn = false;'. But
don't worry, *I* didn't forget about it.
This is basically FIQ's patch from VCE, except he never upstreamed it
because he said something along the lines of it seeming to not fit the
purpose of upstream.
But anyway, it basically just de-duplicates all the text input and text
finishing handling code and cuts down on the large amount of copy-paste
in the editor functions. It makes things way more maintainable.
Interesting note, it seems like FIQ had the intent to refactor the text
input in editor settings (i.e. the level metadata details), but never
got around to it in VCE. Maybe we'll finish that job for him later.
The problem we're running into is entirely contained in the Screen - we need to
either decouple graphics context init from Screen::init or we need to take out
the screenbuffer interaction from loadstats (which I'm more in favor of since we
can just pull the config values and pass them to Screen::init later).
Allowing users to reverse cycle tilesets/tilecols/enemies prevents them
from having to press the hotkey a zillion times in order to get to the
one they want if the one they want just happens to be behind the current
one they're on.
This tilecol conveniently lets players use one of the unpatterned Space
Station tilesets you see on the left side of tiles.png but never get to
use without Direct Mode.
It does have a few weird quirks, but it should be safe to use.
Previously, it was:
if (ed.settingsmod)
{
(Settings menu controls)
...
}
else
{
(Literally everything else
Also a bunch of copy-pasted ed.keydelay checks)
...
}
Now it is:
if (ed.settingsmod)
{
(Settings menu controls)
...
}
else if (ed.keydelay > 0)
{
ed.keydelay--;
}
else if (key.keymap[SDLK_LCTRL] || key.keymap[SDLK_RCTRL])
{
// Ctrl modifiers
...
}
else if (key.keymap[SDLK_LSHIFT] || key.keymap[SDLK_RSHIFT])
{
// Shift modifiers
...
}
else
{
// No modifiers
ed.shiftkey = false;
...
}
It might not counteract how completely huge this code is, but it's at
least organized better.
Also, I had to change the map resize logic around slightly, else it'll
get triggered any time you do a shift modifier keypress.
Their drawframe needs to be incremented by 2 instead of 1, because
they're double-sized.
Animation type 3 is used by the cloud emitter in The Solution is
Dilution, animation type 6 is used by the radar dish in Comms Relay.
Animation type 4 is used by the maverick bus in B-B-B-Busted, but it's
not noticeable since it spawns offscreen. This bug would cause all of
those entities to appear incorrectly for the deltaframes between the
tick the room got loaded and the next tick after that.
This is noticeable in flibit's tweet showing off my over-30-FPS patch:
https://twitter.com/flibitijibibo/status/1273983014930993153
Now that you have a mini menu in MAPMODE, it's a bit annoying to have to
deal with the slowed-down timestep when pressing left/right/ACTION
inside it. Especially since going to an options menu restores the
timestep back to normal (because it's in TITLEMODE). Also removed it
from TELEPORTERMODE for consistency.
That way, they don't show up as "?: something else" and their proper
names are shown.
I didn't update the song numbers to include the newly-allowed songs
because otherwise it'd no longer correlate with what song numbers you
use for the music() simplified command.
This is basically just bolting on the "frames" part of a time trial
score. There's not enough space to properly show it on the time trial
select screen, maybe we can figure something out later. But I at least
want to implement the functionality now.
This doesn't make the editor completely accessible on controller, but
it's a good start at least. VCE already let you move between rooms with
the D-Pad, though.
These functions will only complain once if they receive an out-of-bounds
tile. And it's only once because these functions are called frequently
in rendering code.
A macro WHINE_ONCE() has been added in order to not duplicate code.
Disabling the one-way recolor if assets are mounted is needed to make
existing levels not look bad, but what about levels that want to use the
recolor anyway?
The best solution here is to just introduce another bool into the XML,
and make the re-color opt-in and only present if assets are mounted if
that tag is present.
Some levels (like Unshackled) have decided to manually re-color the
one-way tiles on their own, and us overriding their re-color is not
something they would want. This does mean custom levels with custom
assets don't get to take advantage of the re-color, but it's the exact
same behavior as before, so it shouldn't really matter that much.
I would've liked to specifically detect if a custom tiles.png or
tiles2.png was in play, rather than simply disabling it if any asset was
mounted, but it seems that detecting if a specific file was mounted from
a specific zip isn't really PHYSFS's strong suit.
One-ways have always had this problem where they're always yellow. That
means unless you specifically use yellow, it'll never match the tileset.
The best way to fix this without requiring new graphics or changing
existing ones is to simply re-tint the one-way with the given color of
the room. That way, the black part of the tile is still black, but the
yellow is now some other color.
Ved has this useful feature where you can "lock" a gravity line or warp
line in place, meaning it'll no longer extend its length until it
touches a tile. A line is locked if the p4 of the edentity is 1.
VVVVVV doesn't support this, but now it does. The horrifying thing is
that it stretches the lines out *while rendering the line*, so it looks
like logic and rendering aren't that separate after all (although, I
already learned that when I did my over-30-FPS patch).
This change was half-backported from the localization branch, except I
just came up with "scaling mode" as a better term than the more generic
"graphics mode". It doesn't make sense to still have the option be
called "toggle letterbox" because a third option (integer mode) was
added at some point.
The options for fullscreen and scaling mode were at the top, then there
were various other graphical options, and then the option to resize to
the nearest window size that is of an integer multiple was all the way
below that. Now that last option is moved to be right below the other
options related to window sizing.
VVVVVV's menus are kind of packed to the brim, so I thought it was time
to recategorize the menus a little bit. There's now a new "advanced
options" menu which holds the following options which were moved out of
graphic options, game options and especially accessibility options:
- toggle mouse
- unfocus pause
- fake load screen
- room name background
- glitchrunner mode
I also made the positioning of the titles and descriptions more
consistent, and made some options which were moved to the new menu not
so abbreviated ("load screen" and "room name bg")
For whatever reason, not all arguments of createentity() are exposed in
the command.
We have to keep in mind that (1) unspecified arguments default to 0
(instead of the 320 and 240 for argument 8 and 9 that createentity()
usually defaults to), and that (2) arguments persist across commands.
(Why not get rid of argument persistence, you say? Unfortunately, some
levels rely on argument persistence to call gotoposition() without
specifying the third argument, even though you're supposed to specify
all three arguments.)
To add these arguments without breaking levels, I re-added the
createentity() defaults of 320 and 240 for args 8 and 9, and then I
reset the new arguments afterwards when I'm done. Technically this could
be bad if other commands used those higher arguments, but none of them
really do. (Except createcrewman(), but it only sets argument 6 to 0
sometimes anyway, but argument 6 is already supposed to default to 0.)
The game for some reason had this thing where it would not draw the
diagonal background tiles if you had animated backgrounds turned off.
Which is weird, because spikes with that background are still drawn as
spikes with that background. And also, it doesn't do this for any of the
tower hallway rooms, which is inconsistent.
Better to simplify the logic in Render.cpp anyways by removing
graphics.drawtower_nobackground() and making it really clear what
exactly we'll do if backgrounds are turned off. ("Aren't we already not
drawing the background? What's this _nobackground() function for?")
Flip Mode will now be in the game options menu if either:
(1) You're playing the M&P version.
(2) You have it unlocked and you came here from the in-game pause
screen.
This is because if you're playing M&P, you'd have to close the game,
edit unlock.vvv, and re-launch the game to toggle Flip Mode, since
there's no other way to do so. And if you're playing the full version,
you'd have to save and exit your session in order to toggle Flip Mode.
If you want your game window to simply be exactly 320x240, or 640x480,
or 960x720 etc. then it's really annoying that there's no easy way to do
this (to clarify, this is different from integer mode, which controls
the size of the game INSIDE the window). The easiest way would be having
to close the game, go into unlock.vvv, and edit the window size
manually. VCE has a 1x/2x/3x/4x graphics option to solve this, although
it does not account for actual monitor size (those 1x/2x/3x/4x modes are
all you get, whether or not you have a monitor too small for some of
them or too big for any of them to be what you want).
I discussed this with flibit, and he said that VCE's approach (if it
accounted for monitor size) wouldn't work on high-retina displays or
high DPIs, because getting the actual multiplier to account for those
monitors is kind of a pain. So the next best thing would be to add an
option that resizes to the nearest perfect multiple of 320x240. That way
you could simply resize the window and let the game correct any
imperfect dimensions automatically.
The Alt+Enter Glitch is a funny glitch where if you press any toggle
fullscreen keybind during a cutscene, Viridian will stop moving (if
they're being moved by a walk()) and ACTION will start being held down
for them. Meaning in most cases you can interrupt a walk and flip at the
same time.
This can obviously break cutscenes if a casual just wants to toggle
fullscreen, so I'm fixing it. But it will be unfixed in glitchrunner
mode in case you want to glitch out custom levels (I know I do).
More information on the Alt+Enter Glitch is available here:
https://gitgud.io/infoteddy/vvvvvv-knowledge/blob/master/bugs/bugs.md#the-altenter-glitch
(The page is a bit outdated, some bugs have been fixed in 2.3 already.)
It's sometimes unwanted by people, and it's unwanted enough that there
exist instructions to hexedit the binary to remove it (
https://distractionware.com/forum/index.php?topic=3247.0 ).
Fun fact, the unfocus pause didn't exist in 2.0.
There were a few problems with the old way of doing things:
(1) Level stats were an ad-hoc object. Basically, it's an object whose
attributes are stored in separate arrays, instead of being an actual
object with its attributes stored in one array.
(2) Level filenames with pipes in them could cause trouble. This is
because the filename attribute array was stored in the XML by being
separated by pipes.
(3) There was an arbitrary limit of only having 200 level stats, for
whatever reason.
To remedy this issue, I've made a new struct named CustomLevelStat that
is a proper object. The separate attribute arrays have been replaced
with a proper vector, which also doesn't have a size limit.
For compatibility with versions 2.2 and below, I've kept being able to
read the old format. This only happens if the new format doesn't exist.
However, I also WRITE the old format as well, in case you want to go
back to version 2.2 or below for whatever reason. It's slightly
wasteful to have both, but that way there's no risk of breaking
compatibility.
Centiseconds won't be saved to any save file or anything. This is just
to make speedrunning a bit more competitive, being able to know the
precise time of a time trial or full game run.
The time trial par time on the result screen always has ".99" after it.
This is basically due to the game comparing the number of seconds to the
par number of seconds using less-than-or-equal-to instead of simply
less-than.
The only reason why gray Warp Zone entities were green originally was
because there is a giant concatenated list of tileset+tilecol
combinations, and by using tileset 3 tilecol 6 you're using the entry
of tileset 4 tilecol 0, which is the green Ship tileset.
So without interfering with the green Ship tileset's entry, I've decided
that the best thing to do is to just add special cases. The enemy color
was easy enough to fix. The platform color was also easy to fix.
However, there exist no existing textures for gray conveyors, so at that
point I decided to just tint the existing green one gray, and then I did
the same for platforms.
This patch is very kludge-y, but at least it fixes a semi-noticeable
visual issue in custom levels that use internal scripts to spawn
entities when loading a room.
Basically, the problem here is that when the game checks for script
boxes and sets newscript, newscript has already been processed for that
frame, and when the game does load a script, script.run() has already
been processed for that frame.
That issue can be fixed, but it turns out that due to my over-30-FPS
game loop changes, there's now ANOTHER visible frame of delay between
room load and entity creation, because the render function gets called
in between the script being loaded at the end of gamelogic() and the
script actually getting run.
So... I have to temporary move script.run() to the end of gamelogic()
(in map.twoframedelayfix()), and make sure it doesn't get run next
frame, because double-evaluations are bad. To do that, I have to
introduce the kludge variable script.dontrunnextframe, which does
exactly as it says.
And with all that work, the two-frame (now three-frame) delay is fixed.
It's annoying for casuals to have to close the game if they manage to
get themselves to turn into VVVVVV-Man, but it's amusing enough to
glitchrunners that they mess about with VVVVVV-Man in the main game,
clipping through walls everywhere (well, they call it Big Viridian, but
still).
Ironically enough, resetting more variables in script.hardreset() makes
the glitchy fadeout system even more glitchy. Resetting map.towermode,
for example, makes it so that if you're in towers when you quit to the
menu, script.hardreset() makes it so that the game thinks you're no
longer inbounds (because it no longer thinks you're in a tower and thus
considers coordinates in the space of 40x30 tiles to be inbounds instead
of 40x700 or 40x100 tiles to be inbounds), calls map.gotoroom(), which
resets the gamestate to 0. So if we're using the old system, it's better
to reset only as much as needed.
And furthermore, we shouldn't be relying on script.hardreset() to
initialize variables for us. That should be done at the class
constructor level. So I've gone ahead and initialized the variables in
class constructors, too.
This was fixed in 2.3 because one of the side effects of this janky
system was being able to accidentally immediately quit to the title if
the screen was black during a cutscene, which is something very likely
to happen to casual players.
Anyway, credits warp uses this gamestate-based system because it
utilizes quitting to the title screen doing gamestate 80. From there,
you increment the gamestate to gamestate 94 to use the Space Station 2
expo script.
This is the second part of what is necessary for credits warp to work.
The speedrunners call this "text storage". You need to get the
advancetext prompt up without a text box in order to be able to
increment the gamestate without bound. In 2.0, script.hardreset() reset
the text boxes, but not the prompt.
This is the first part of what is necessary for credits warp to work.
If the "- Press ACTION to advance text -" prompt is up, and you manage
to keep it up, then you can indefinitely increment the gamestate by
pressing ACTION.
This is first used in credits warp to teleport to the start of Space
Station 2 (by utilizing the Eurogame expo script, triggered by a
gamestate), and then again later by using a teleporter that has a high
gamestate number to increment to the [C[C[C[C[Captain!] cutscene.
Glitchrunner mode is intended to re-enable glitches that existed in
older versions of VVVVVV. These glitches were removed because they could
legitimately affect a casual player's experience. Glitches like various
R-pressing screwery, Space Station 1 skip, telejumping, Gravitron
out-of-bounds, etc. will not be patched.
It should've checked the final spacing and not the intermediate maximum
value. I had changed some things around, and now the minimum spacing
was 5 instead of 0 by mistake.
All menus had a hardcoded X position (offset to an arbitrary starting
point of 110) and a hardcoded horizontal spacing for the "staircasing"
(mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or
something else). Not all menus were centered, and seem to have been
manually made narrower (with lower horizontal spacing) whenever text
ran offscreen during development.
This system may already be hard to work with in an English-only menu
system, since you may need to adjust horizontal spacing or positioning
when adding an option. The main reason I made this change is that it's
even less optimal when menu options have to be translated, since
maximum string lengths are hard to determine, and it's easy to have
menu options running offscreen, especially when not all menus are
checked for all languages and when options could be added in the middle
of a menu after translations of that menu are already checked.
Now, menus are automatically centered based on their options, and they
are automatically made narrower if they won't fit with the default
horizontal spacing of 30 pixels (with some padding). The game.menuxoff
variable for the menu X position is now also offset to 0 instead of 110
The _default_ horizontal spacing can be changed on a per-menu basis,
and most menus (not all) which already had a narrower spacing set,
retain that as a maximum spacing, simply because they looked odd with
30 pixels of spacing (especially the main menu). They will be made even
narrower automatically if needed. In the most extreme case, the spacing
can go down to 0 and options will be displayed right below each other.
This isn't in the usual style of the game, but at least we did the best
we could to prevent options running offscreen.
The only exception to automatic menu centering and narrowing is the
list of player levels, because it's a special case and existing
behavior would be better than automatic centering there.
The tilesheets in question are font.png, tiles.png, tiles2.png,
tiles3.png, entcolours.png, teleporter.png, sprites.png, and
flipsprites.png.
This patch removes the hardcoded dimensions when scanning the
tilesheets, because it's simpler that way. It also de-duplicates it so
it isn't a bunch of copy-paste, by using macros. (I had to use macros
because it was the easiest way to optionally pass in some extra code in
the innermost for-loop.)
Also, if the dimensions of a scanned tilesheet aren't exactly multiples
of the dimensions of the tile unit for that given tilesheet (e.g. if the
dimensions of a scanned tiles.png are not exact multiples of 8), then an
SDL_SimpleMessageBox will show up with the error message, a puts() of
the error message will be called, and the program will exit.
It seems like they were unfinished. This commit makes them properly
work.
When a track is stopped with stopmusic() or musicfadeout(),
resumemusic() will resume from where the track stopped. musicfadein()
does the same but does it with a gradual fade instead of suddenly
playing it at full volume.
I changed several interfaces around for this. First, setting currentsong
to -1 when music is stopped is handled in the hook callback that gets
called by SDL_mixer whenever the music stops. Otherwise, it'd be
problematic if currentsong was set to -1 when the song starts fading out
instead of when the song actually ends.
Also, music.play() has a few optional arguments now, to reduce the
copying-and-pasting of music code.
Lastly, we have to roll our own tracker of music length by using
SDL_GetPerformanceCounter(), because there's no way to get the music
position if a song fades out. (We could implicitly keep the music
position if we abruptly stopped the song using Mix_PauseMusic(), and
resume it using Mix_ResumeMusic(), but ignoring the fact that those two
functions are also used on the unfocus-pause (which, as it turns out, is
basically a non-issue because the unfocus-pause can use some other
functions), there's no equivalent for fading out, i.e. there's no
"fade out and pause when it fully fades out" function in SDL_mixer.) And
then we have to account for the unfocus-pause in our manual tracker.
Other than that, these commands are now fully functional.
This fixes a corner case where using gamestate 82 from the editor would
put you in a softlock because it would return to the editor settings
menu, which only functions in EDITORMODE and results in a softlock in
TITLEMODE.
This is already done for invincibility. It's kind of unnecessary, but
it's just to make sure if for some reason in the future variables like
insecretlab/intimetrial/nodeathmode don't get reset when exiting to the
menu.
To fix this annoying flicker (which, btw, took me WAY too long to do), I
had to introduce yet another kludge variable to signal that the
horizontal/vertical warp background should be re-initialized on the
pause screen.
I think I could technically keep the 'graphics.backgrounddrawn = false;'
in maplogic() and remove the 'graphics.backgrounddrawn = false;' in
Game::returntopausemenu(), but I'm keeping that other one around because
it doesn't hurt and just as a general precaution and safety measure.
This makes it so the tower background doesn't persist and scroll upwards
if you exit the menu in a warp zone horizontal or vertical room.
Ugh, and while we're on the subject of separating the in-game tower
background and the menu tower background, could we PLEASE separate the
horizontal / vertical warp backgrounds from the tower backgrounds, too?!
Another thing that's annoyed me a lot is being unable to simply press
Esc to close the pause menu. You'd have to hover over the "return to
game" or "keep playing" option. This would be even more annoying with
more options on the menu, so allowing to press Esc is a nice
quality-of-life thing.
Otherwise you could keep re-pressing ACTION on the "yes" option and keep
stalling it until it finally faded out, or quickly go back past menu
options or something.
Otherwise the menu background would have this rendering glitch where the
bypos of the in-game tower wouldn't divide easily and have a bunch of
jitters in an otherwise smooth but overall still somewhat smooth
background.
Also set map.tdrawback to true when leaving the menu.
This is to fix the interpolated color of the tower background
persisting, as well as making sure the menu background doesn't persist
when exiting.
This would be fine, under the assumption that you could never reach the
menu from outside the menu. Well, now you can, so now this has to play
the correct song instead of track 6.
This is to pre-emptively prevent piling up stack frames for what I'll be
adding next, which is pressing Esc in the options menu in-game
automatically moving you back to MAPMODE.
Since the exact same tower background is also used on the menu, we need
to save the current state of the background when entering the menu
(before overwriting it), and then put it back when we're done. Maybe we
ought to separate the in-game and menu tower backgrounds...
This also fixes a semi-hilarious bug where you could make Panic Room go
in the other direction by simply going to the options menu in-game.
This is accomplished by adding convenience functions
mapclass::bg_to_kludge() and mapclass::kludge_to_bg().
Well this is a bit annoying. I can call graphics.updatetowerbackground()
just fine, but I have to get at the title color update routine inside
titlelogic(), which is hard-baked in. So I have to pull that code
outside of the function, export it in the header, and then call it when
I transition to TITLEMODE.
So now the options do what they do. However, I still need to fix the
1-frame glitch when switching to TITLEMODE, as well as make returning
from the menu return back to MAPMODE, as well as making this better menu
integrate seamlessly with the existing menus.
This prevents from having to repeat 'if (game.menupage == ...)'
everywhere, which makes for more concise code.
I know you're technically supposed to indent the cases surrounded by
if-guards, but I don't think indenting them here would help anything.
I'd only indent it if the 'if' had an 'else', for example. But if it
surrounds the whole case, then there's no need for indentation.
Similar to Graphics::map_tab(), this ensures that I don't have to
copy-paste printing the map options for every single game.menupage case
I want, and in this case that's a good thing because there'll be 4
game.menupage cases I'll be using.
This basically adds an extra '|| game.inintermission' because it seems
like the original code forgot about that conditional. You can't save in
level replays, so there's no need to say "You will lose any unsaved
progress." in intermission replays.
In my previous PR, I wrongly assumed that I could just replace the `i`
handling code of those options with an `i++;` at the beginning, and thus
I could put all blocks' `i++;` into ARG_INNER(). Well I was wrong,
because the code was written the original way for a reason, namely that
we still need `i` to point to the -playx/y/rx/ry/gc/music argv so we can
re-compare which argument led us into this code block.
Any decent compiler will optimize this so that it's still only two
function calls (or it gets inlined). However, it's still not very
readable, so I've assigned the result to a variable and used that
instead.
Instead of copy-pasting everything and changing a few parts, just have
something that handles that tiny part. This reduces the amount of code
size the custom level CREW page takes up by half.
This will be used to keep some text positions the same when in Flip
Mode, instead of having to copy and paste code.
This function being at the very top of the file kind of violates
locality, but it has to be done because it can't be a macro.
Previously, the code to print all tab names was copied to every single
tab, resulting in 12 more superfluous print statements than needed. This
commit uses graphics.map_tab to de-duplicate all the code.
This function is useful to de-duplicate all the map page names at the
bottom, which are MAP, CREW/SHIP/GRAV, STATS, and SAVE. If selected, it
will surround the text in square brackets and automatically handle the
positioning.
Shamelessly copy-pasted from Dav999's localization branch.
Just to be helpful if someone has a failing FILESYSTEM_init(), but
doesn't know that's their issue and keeps wondering why VVVVVV just
exits with code 1.
The command-line argument parsing code has a lot of copy-paste. This
copy-paste would get even worse if I added safety checks to make sure
you couldn't index argv out-of-bounds by having an argument like
`-renderer` without having anything after it, i.e. you'd be doing the
command `./VVVVVV -renderer`.
Previously, only the playtest arguments (apart from the recently-added
`playassets`) had this safety check, but the message it printed whenever
the safety check failed was always "-playing option requires one
argument" regardless of whatever argument actually failed to be parsed.
So I fixed it so that all arguments actually output the correct
corresponding failed argument instead.
Also, `strcmp(argv[i], <name>) == 0` is really kind of noisy, even if
you understand what it does perfectly well.
So I refactored it with a bunch of macros. ARG() just does the strcmp()
char* comparison, and ARG_INNER() does the safety check and returns 1,
along with printing a message, if the safety check fails.
This is used if you're loading a level file from STDIN. The game needs
to know the actual level assets directory you're referring to, since
when it gets the level from STDIN, it doesn't know the actual filename
of the level.
Fixes#309.
The assets mounting code was put directly in editorclass::load(), but
now it's in a neat little function so it can be called from multiple
places without having to call editorclass::load().
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!
Ugh, this is terrible and stupid and I hate myself for it.
Anyway, since the SDL2 VSync hint only applies when the renderer is
created, we have to re-create the renderer whenever VSync is toggled.
However, this also means we need to re-create m_screenTexture as well,
AND call ResizeScreen() after that or else the letterbox/integer modes
won't be applied.
Unfortunately, this means that in main(), gameScreen.init() will create
a renderer only to be destroyed later by graphics.processVsync().
There's not much we can do about this. Fixing this would require putting
graphics.processVsync() before gameScreen.init(). However, in order to
know whether the user has VSync set, we would have to call
game.loadstats() first, but wait, we can't, because game.loadstats()
mutates gameScreen! Gahhhhhh!!!!
@leo60228 suggested to fix that problem (
https://github.com/TerryCavanagh/VVVVVV/pull/220#issuecomment-624217939
) by adding NULL checks to game.loadstats() and then calling it twice,
but then you're trading wastefully creating a renderer only to be
destroyed, for wastefully opening and parsing unlock.vvv twice instead
of once. In either case, you're doing something twice and wasting work.
Otherwise a NO_CUSTOM_LEVELS build would fail. Also I got rid of the
'graphics.flipmode = false;' in the fixed loop because I don't think it
does anything.
This is needed for the next step. I want to put all the loop stuff in
their own functions so the code isn't one huge blob, but to do that I'll
need to make 'time' a global variable, but I can't do that because
actually 'time' is already a function, apparently, and you're only
allowed to shadow variables when already inside a function.
Like actual entities, editor ghost colors were updating every render
frame instead of logic frame. So just like actual entities, I added a
realcol attribute to them that editorrender() uses instead, and added
code to update said realcol attribute in editorlogic(). That way the
colors don't go by too quickly (especially the death color).
Just like all the other fixes, the variable that controls the amount of
ghosts to show was being updated every render frame instead of every
logic frame.
This is because due to the game loop changes in this over-30-FPS patch,
editorrender() can be called and undo graphics.backgrounddrawn being set
to false once again. Solution here is to make it so it keeps being set
to false until game.shouldreturntoeditor is turned off, which has also
been moved to editorlogic().
This is to make sure no lerping occurs in 30-FPS mode, otherwise things
might look weird. A good case that this fixes is how entities look in a
double-gotoroom (they should be completely frozen in 30-FPS mode).
There is now an option in "graphic options" named "toggle fps", which
toggles whether the game visually runs at 1000/34 FPS or over 1000/34
FPS. It is off by default.
I've had to put the entire game loop in yet another set of braces, I'll
indent it next commit.
What happens here is that the entity gets created and then gets
immediately updated on the next frame, but there's no time for their
walkingframe of 0 to be rendered, so it'll look like they have just
started with walkingframe 1. However in the delta-timestep rendering
it'll render with walkingframe 0. So we need to fix their drawframe and
increment it when creating them.
Looks like mapclass::changefinalcol() is called whenever you enter a
room in Outside Dimension VVVVVV.
mapclass::changefinalcol() changes the tile, but it doesn't update their
drawframe. So after that function is called, update their drawframe.
If you update their drawframe while inside that function, then when the
platform actually cycles color it'll cycle backwards quickly sometimes,
which is not ideal.
When I loaded the room where Vitellary is in Space Station 2, I saw this
1-frame glitch happen despite my previous efforts to prevent it. So now
it's fixed.
This is because otherwise, on my Linux system at least, the game will
take up a lot of CPU that it doesn't really need to (I only have a 60hz
monitor).
On Windows, it looks like Windows already enforces V-sync for
applications anyway, unless they have exclusive fullscreen control.
Linux doesn't enforce V-sync on apps and lets them take up as much CPU
as they want, so I'm putting this here to limit the framerate.
The game is already actually 30 FPS anyway, the nice smooth FPS is just
visual. V-sync won't introduce any more "input lag" than already exists.
By "hidden names", I'm referring to "Dimension VVVVVV" and "The Ship"
popping up on the quit/pause/teleporter screens, even though those rooms
don't have any roomnames.
Apparently my commit to fix roomname re-draw bleed on the
quit/pause/teleporter screens exposed yet another hardreset()-caused
bug. The issue here is that since hardreset() sets game.roomx and
game.roomy to 0, map.area() will no longer work properly, and since the
hidden roomname check is based on map.area(), it will no longer display
"Dimension VVVVVV" or "The Ship" once you press ACTION to quit. It used
to do this due to the re-draw bleed, but now it doesn't.
I saw that roomnames didn't get reset in hardreset(), so the solution
here is to re-factor hidden names to be an actual variable, instead of
being implicit. map.hiddenname is a variable that's set in
mapclass::loadlevel(), and if isn't empty, it will be drawn on the
quit/pause/teleporter screens. That way it will still display "Dimension
VVVVVV" and "The Ship" when you press ACTION to quit to the menu.
EDIT: Since PR #230 got merged, this commit is no longer strictly
necessary, but it's still good to refactor hidden names like this.
As much as it looks cool to have a slowly-scrolling background on the
title screen, it's quite annoying that slowmode applying on the title
screen mean that your keypresses are less responsive.
This adds Graphics::crewcolourreal(), which is like the
entityclass::crewcolour() that the editor already uses, except for the
real color instead of the color ID. Also, editorclass now has an
attribute `entcolreal` so enemy colors don't update more than 30 frames
a second.
The solution is to draw another row of incoming textures. And also just
draw another row of textures when the background needs to be redrawn,
otherwise it'll flicker when the color changes while you're holding down
ACTION.
To fix this, I draw another row/column of incoming textures. But of
course, I have to extend the size of the towerbuffer, otherwise the
incoming textures will just be gone.
This could happen if you held down ACTION in the credits, looks like the
background doesn't keep up for some reason. That's another bug to fix,
but at least I can fix this overdraw.
This is only noticeable if you have a font with translucent pixels, like
I do. But it gets really noticeable really quickly with this over-30-FPS
patch because the render functions are continuously called every
delta-timestep. To prevent this, just fill the backbuffer with black
before rendering anything.
There's still a problem in that the flickering that would lead to this
overdraw in the first place still exists. But at least if it'll flicker,
it'll flicker black and not overdraw.
Currently it interpolates it based on the current state of game.swngame,
but when game.swngame changes the interpolation doesn't know that it has
JUST changed or anything. So add a kludge variable to fix this
off-by-one.
This was especially noticeable in slowmode, where after going to an
adjacent room, it would look like they stopped for a split second. This
commit makes it so they smoothly continue their journey after switching
rooms.
The integer cast is to round off any fractional part of the velocity so
that they don't make a difference and result in the oldxp/oldyp being
one pixel off. Especially since the player's y-velocity fluctuates while
standing unflipped on the floor.
Incidentally enough, this seems to only have been a problem with screen
transitions for some reason. No other uses of gotoroom() (such as the
one where gotoroom() is called every other frame, or every frame) seem
to have resulted in this "pausing" behavior, or at least a reversion
back to 30 FPS movement. I don't know why.
Otherwise it'll go by too quickly.
Also something subtle here - I didn't make it conditional on
game.advancetext, so now it'll still decrement even if you have
advancetext up.
This makes editor notes fade out smoothly. And even though the notedelay
only gets decremented by one every editor-frame (the editor runs at
1000/24 FPS fixed-timestep here), it actually gets multiplied by 4, so a
floating-point interpolated value would make a difference here.
This is because their oldxp wasn't being updated when they move (or
rather, teleport) and wrap around the screen.
These enemies are ZZT centipedes, but they're referred to as ASCII
snakes in comments in the code.
These colors were of the colors of each crewmate, the inactive crewmate
color, and the color of the trinket and clock on the quicksave/summary
screens.
These colors all used fRandom() and so kept updating too quickly because
they would be recalculated every time the delta-timestep render function
got called, which isn't ideal. Thus, I've had to add attributes onto the
Graphics class to store these colors and make sure they're only
recalculated in logic functions instead.
Thankfully, the color used for the sprites on the time trial results
screen doesn't use fRandom(), so I don't have to worry about those.
There's a new version of Graphics::drawsprite() that takes in a pre-made
color already, instead of a color ID. As well, I've also added
Graphics::updatetitlecolours() to update these colors on the title
screen.
Okay, so the problem here is that Graphics::setcol() is called right
before a sprite is drawn in a render function, but render functions are
done in deltatime, meaning that the color of a sprite keeps being
recalculated every time. This only affects sprites that use fRandom()
(the other thing that can dynamically determine a color is help.glow,
but that's only updated in the fixed-timestep loop), but is especially
noticeable for sprites that flash wildly, like the teleporter, trinket,
and elephant.
To fix this, we need to make the color be recalculated only in the
fixed-timestep loop. However, this means that we MUST store the color of
the sprite SOMEWHERE for the delta-timesteps to render it, otherwise the
color calculation will just be lost or something.
So each entity now has a new attribute, `realcol`, which is the actual
raw color used to render the sprite in render functions. This is not to
be confused with their `colour` attribute, which is more akin to a color
"ID" of sorts, but which isn't an actual color.
At the end of gamelogic(), as well as when an entity is first created,
the `colour` is given to Graphics::setcol() and then `realcol` gets set
to the actual color. Then when it comes time to render the entity,
`realcol` gets used instead.
Gravitron squares are a somewhat tricky case where there's technically
TWO colors for it - one is the actual sprite itself and the other is the
indicator. However, usually the indicator and the square aren't both
onscreen at the same time, so we can simply switch the realcol between
the two as needed.
However, we can't use this system for the sprite colors used on the
title and map screen, so we'll have to do something else for those.
In order to make sure colors don't update more than 1000/34 frames per
second, I'll have to move the color-setting part of this function
somewhere else.
Just a small thing, but if you teleported out of a tower with the
top/bottom screen spikes being onscreen (by dying, for example), they
would retract once you went back in to a tower. Small little thing, but
it's a good thing to polish.
Otherwise, the tile animations will go too fast. However, the overall
color cycling hasn't been going fast, since it was never in gamerender()
in the first place.
When you pressed and released ACTION to speed up the credits, the
credits position would end up being 1 frame off from the background.
This is due to the fact that we update the tower background after we
update the credits position, so this commit moves the tower background
update before the credits position update.
These special images are the crewmates, Level Complete, and Game
Complete images. They flashed depending on if you were lucky and
happened to got your delta-timesteps just right when text boxes were
fading in and out.
Honestly, I'm surprised text box fading in/out hasn't ran into this
issue before. It's insane luck that this issue hasn't occurred before or
anything.
Well, anyways, to fix this, there's now an attribute `allowspecial` on
text boxes, and an optional parameter of the same name for
Graphics::createtextbox(). This attribute is the only thing that will
let these special text box images render. And any createtextbox()es that
utilize these special images have been updated accordingly.
By "frame" here I'm referring to the fixed-timestep, not a visual
delta-timestep.
Anyway, the problem is because crewmates' drawframes wait for
entityclass::animateentities() to be called in gamelogic(). In the
old, 30-FPS-only system, this entityclass::animateentities() would be
called in gamerender(), before any actual rendering would take place.
However, I've had to move it out of gamerender() because otherwise
entities would animate too fast. As a result, since gamerender() could
be called in between the entity creation and gamelogic(), a
less-than-1-frame visual glitch could happen.
The solution is to set the entity's drawframe as well when fixing facing
the player in Map.cpp.
Incidentally enough, this de-duplicates the amount of times this code
has been copy-pasted from 4 times to 2.
Anyways, this makes it so the crew don't go lightning fast on the
summary screen, the NDM game-over screen, the NDM win screen, and the
pause screen in the main game. It was slightly hilarious seeing them go
really quickly, actually. It was like they were running away from a
giant monster or something.
Just to make sure it's extra smooth. Not that it will be noticeable, and
you can't access the Secret Lab in slowmode without modifying the game,
but it's nice to have this.
Otherwise it'll go by really fast and rapidly pulsate. To the point
where it seems like it would be an epilepsy trigger, although I
wouldn't know anything about epilepsy other than that it's bad.
Ok, now THIS takes the cake for "only really noticeable in slowmode",
because it only ever moves at 1 pixel per second. And even then,
slowmode shouldn't apply on the title screen, so it won't even show up
there once I get around to doing that change.
This is so the background doesn't NYOOOOM past at light speed. Although
for a game set in space like VVVVVV, light speed ain't bad.
And this finally requires that editorlogic() have a call to
Graphics::updatebackground().
To make it real smooth, just in case it was noticeable that it only
updated at 1000/34 FPS before (well, except in slowmode, it's really
noticeable THERE).
Also this removes the re-typing out of (game.act_fade/10.0f) for every
single R, G, and B in gamerender().
This makes text boxes fade in and out pretty smoothly.
This requires that the textboxclass::setcol() be in Graphics::drawgui(),
so now it's moved there.
Text box fading is only really noticeable if you're playing in slowmode.
This has to be done in order to fix rendering when on a conveyor or
moving platform and actively moving with or against it. Pretty sure this
shouldn't break anything, oldxp/oldyp is mostly visual after all (and by
the time it's used for gravity line collision checking,
updateentitylogic() would've already gotten around to it anyway).
Incidentally, this also fixes a jitter that would occur if you were
moving at the time you died or collected a trinket or custom crewmate,
due to the game temporarily freezing and either doing deathsequence or
completestop.
Now it's really, really smooth. Except for like the last frame when it
goes down, which I sometimes didn't notice (but maybe it didn't happen
every time due to being lucky on the delta timesteps or something,
whatevs.)
Since "if (graphics.resumegamemode)" and "if (menuoffset > 0)" both do
the same thing, they've been combined with an "or" conjunction.
As well, the map.extrarow check in maplogic() has been refactored to use
a variable instead of duplicating the entire code block. Not that it
matters anyway, because the difference between 240 and 230 is only 10
pixels, far short of the 25 pixel increment that bringing the menu up
and down uses, and both 240 and 230 integer-divided by 25 have the same
non-remainder value of 9.
Otherwise the screen will shake too fast for my liking.
Also I'm planning to add an FPS limiting option later (because right
now, un-capping the FPS is pretty wasteful and eats up lots of
resources, especially since I have only a 60hz monitor), and it'd feel
weird if screen shaking updated every delta timestep.
This fixes entities being drawframe 0 for 1 frame when being first
created. Incidentally, this also fixes entities created during
completestop being the player sprite, too, which is something not many
people notice.
For some reason, it was put near the start of gamerender(), even though
since it handles edge-flipping it seems like it should be in the logic
function already.
This makes sure entity animations don't animate as fast as possible, and
also fixes edge-flipping on normal surfaces.
This prevents undefined behavior because we use oldxp/oldyp to do linear
interpolation.
It's also initialized in entclass::entclass(), just to be sure. And I've
deduplicated the regular xp/yp initialization in createentity(), too.
I've added a function Graphics::lerp() which simply interpolates between
two values given a certain alpha value. It's just like drawing a
straight line between two points.
Also, Graphics now has an `alpha` attribute, and it is set on every
deltatime update to be used in linear interpolation.
Ok, and this is where the fun starts.
In an ideal world, this would be the end of this patch. However, of
course, there are many, MANY places in the game that update
fixed-timestep timers DIRECTLY inside the render function, which is not
ideal because it means those timers go super fast.
I'll have to fix those later.
Ok, NOW indent it. I didn't indent it previously because the diffs are
annoying to read if there's an indent that doesn't otherwise change
anything (and even now it's pretty annoying to read).
Alright, this is the start of the over-30-FPS patch!
First things first, we'll need to make it possible to have a separate
deltatime loop outside of the fixed timestep loop. And for that, we
can't be using SDL_Delay(), as SDL_Delay() (as you might imagine) blocks
the whole program.
Instead we'll be using this thing called an accumulator. It looks at how
long the previous poll took (the raw deltatime), and lets timesteps pass
accordingly.
On a side note, I've had to split the `time` and `timePrev` declaration
each onto their own separate line, otherwise there's undefined behavior
from `time` not being initialized.
I use `accumulator = fmodf(...)` instead of `accumulator -=
timesteplimit` because otherwise it'll fast-forward if it's behind,
which is a jarring thing to see.
Also in preparation for what's going to come down the over-30-FPS road,
I've also added `deltatime` and `alpha`. `deltatime` is going to be used
if the game is in slowdown mode, and `alpha` is going to be used for
linear interpolation of animations.
By the way, what was the main game loop previously (and is now the new
timestep loop) is now in an extra set of curly braces, but I haven't
indented it yet to reduce the noise in this commit.
This prevents being able to "roll over" the amount of minutes to 0 (by
simply waiting for the timer to tick past one hour) and being able to
get a result of 00:13 when your result is really 01:00:13.
By looking only at the minutes, the game would read 01:00:13 as 00:13
instead. So simply add the amount of hours to the time trial result.
This is needed for MinGW when compiling C++98, apparently. I put it in
an if-guard because otherwise there'll be a warning from MY compiler
about redefinitions.
If you exited to the menu normally (i.e. got on a code path that went
through Game::quittomenu()), the menu music wouldn't play. This is
because FILESYSTEM_unmountassets() was put after music.play(6). So the
game would play the custom level's track 6, and then unmount it, which
meant it could no longer play track 6, but there's nothing telling the
game to play track 6 again. So I just changed the frame ordering around.
I also added a comment to make sure anyone reading the code is aware of
the frame order dependency.
If you exited from the editor, custom assets would not be unmounted. But
I made sure to put the FILESYSTEM_unmountassets() before the
music.play(6) because otherwise the menu music wouldn't play.
You could also exit to the menu from a custom level using the
rollcredits() command, so I made sure to put a
FILESYSTEM_unmountassets() when returning to the menu from the credits
as well. I also made sure to put it before the music.playef(18) so
there's no risk of the sound effect not playing properly, or not playing
the non-level-specific one.
I added a comment to both FILESYSTEM_unmountasset()s to make sure anyone
reading the code is aware of the frame order dependency.
This is really awful, but there's not much we can do.
TinyXML-2 no matter what will never stop on newlines, so without
changing the XML parser, this is the best we can do - just remove the
"\n " (that's a linefeed plus exactly 12 spaces) if it
appears at the end of the contents of an edentity tag.
Also a giant comment for good measure.
Looks like duplicate player entities persisting across rooms is a
semi-useful feature used by some levels. Still, though, it's a bit of a
nuisance to have duplicate player entities persisting across game
sessions. And levels can't rely on this persistence anyway, anyone could
just close the game and re-open it to get rid of the duplicate entities
regardless.
If a trinket or crewmate ID is out-of-bounds, it will not be created.
This is not only because the `collect`/`customcollect` check in
entityclass::createentity() would then be out-of-bounds, but also
touching it would also be out-of-bounds, too.
Display trinkets will always be created if the ID is out-of-bounds.
Apparently some people (@AllyTally) have been creating display trinkets
with IDs of -1 in order to get a display trinket that always shows up,
which is rather horrifying. But it makes sense, there's a lot more
nonzero int values than there are the amount of int values that are
zero, so it's fairly likely that the `collect` check will always come up
to be true (nonzero). Also, it's more useful to be able to have a
display trinket that always shows up without having to collect a trinket
beforehand, than it is to have it not be created (because technically by
default, you're already in the world where you don't have it created).
Display trinkets still have their `para` set to their ID, though, and if
they managed to gain an `onentity` of 1, bad things could happen... So
just to be sure, I added INBOUNDS checks to crewmates and trinkets in
entityclass::updateentities() so no UB will happen if you collect a
crewmate/trinket with an out-of-bounds ID. Also, I de-duplicated the
`collect`/`customcollect` setting, too.
The flashy color of the elephant can be hard on people's eyes,
especially if they're the type who want screen effects disabled because
they might have epilepsy. The elephant takes up a good 3/4ths of the
screen, you know. If screen effects are disabled, the elephant will use
color 22, which is a neutral gray.
I'm only adding this because the VVVVVV speedrun mods (@tzann, @mohoc)
invalidate all runs that have the elephant texture removed, even though
many people would be looking at a potentially epilepsy-inducing image
many times a day grinding 100% speedruns. (Imo, their justification for
this is flimsy at best.)
The only class that actually needs its i/j/k kept is scriptclass,
because some custom levels rely on it for creating custom activity
zones. So I haven't touched that.
Other than that, there's no chance that anything important relies on
i/j/k in any other class. For that to be the case, it would have to use
i/j/k without initializing it beforehand, and that can simply be
detected by removing the attribute from the header file and seeing where
the compiler complains. And the compiler complains only about cases
where it's initialized first. (Note that due to this check, I *haven't*
removed Graphics's `m` as it precisely does exactly this, using it
without initializing it first.)
Interestingly enough, otherlevelclass and towerclass have unused i/k
variables for whatever reason.
This prevents the game from being saved if you manage to trigger a
savetele() during a "special" gamemode (like if you use the Gravitron
out-of-bounds glitch when replaying Intermission 2, then go to Game
Complete that way).
If you don't have a font.txt, it could happen that a font index is
requested that's out-of-bounds. And that would result in a segfault. So
to fix that I'm adding INBOUNDS checks to all functions that index the
fontmap.
This fixes indexing out-of-bounds in the functions that draw all the
special images such as the elephant and teleporters. Let's make sure the
game doesn't segfault.
I tracked down all the functions that took in an entity's drawframe and
made sure that no matter what value an entity's drawframe was, the game
would never segfault.
To deal with using a different image file for Flip Mode, it looks like
copy-paste was used. This isn't exactly maintainable code. So I'm
replacing it with a reference that changes depending on if the game is
in Flip Mode or not, instead.
Removing the player entity has all sorts of nasty effects, such as
softlocking the game because many inputs require there to be a player
present, such as opening the quit menu.
The most infamous glitch to remove the player entity is the Gravitron
Fling, where the game doesn't see a gravity line at a specific
y-position in the current room, and when it moves the bottom gravity
line it moves the player instead. When the gravity line gets outside the
room, it gets destroyed, so if the player gets dragged outside the room,
they get destroyed, too. (Don't misinterpret this as saying anytime the
player gets dragged outside the room, they get destroyed - it's only the
Gravitron logic that destroys them.)
Also, there are many places in the code that use entity-getting
functions that have a fallback value of 0. If it was possible to remove
the player, then it's possible for this fallback value of 0 to index
obj.entities out-of-bounds, which is not good.
To fix this, entityclass::removeentity() is now a bool that signifies if
the entity was successfully removed or not. If the entity given is the
player (meaning it first checks if it's rule 0, just so in 99% of cases
it'll short-circuit and won't do the next check, which is if
entityclass::getplayer() says the indice to be removed is the player),
then it'll refuse to remove the entity, and return false.
This is a change in behavior where callers might expect
entityclass::removeentity() to always succeed, so I changed the
removeentity_iter() macro to only decrement if removing the entity
succeeded. I also changed entityclass::updateentities() from
'removeentity(i); return true;' to 'return removeentity(i);'.
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.
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.
Continuing from #280, another potential source of out-of-bounds indexing
(and thus, Undefined Behavior badness) comes from script commands. A
majority of them don't do any input validation at all, which means the
potential for out-of-bounds indexing and segfaulting in custom levels.
So it's always good to add bounds checks to them.
Interesting note, the only existing command that has bounds checks is
the flag() command. That means you can't turn out-of-bounds flags on or
off. But there's no bounds checks for ifflag(), or customifflag(), which
means you CAN index out-of-bounds with those commands! That's a bit bad
to do, so.
Also, I decided to add the bounds checks for playef() at the
musicclass::playef() level, instead of just the level of the playef()
command. I don't know of any other cases outside of the command where
musicclass::playef() will index out of bounds, but musicclass is the one
containing the indexed vector anyway, I wanted to cover more cases, and
it's better to be safe than sorry.
And this the function with the least amount of cases where its sentinel
value is used unchecked. Thankfully. obj.getplayer() was a bit of a slug
to get through.
obj.getplayer() can return -1, which can cause out-of-bounds indexing of
obj.entities, which is really bad. This was by far the most changes, as
obj.getplayer() is the most used entity-getting function that returns
-1, as well as the most-used function whose sentinel value goes
unchecked.
To deal with the usage of obj.getplayer() in mapclass::warpto(), I just
added general bounds checks inside that function instead of changing all
the callers.
A few months ago, I added ghosts to the VVVVVV: Community Edition editor. I was told recently I should think
about upstreaming it, and with Terry saying go ahead I finally ported them into VVVVVV. There's one slight
difference however--you can choose whether you have them or not in the editor's settings menu. They're off by
default, and this is saved to the save file.
Anyway, when you're playtesting, the game saves the players position, color, room coordinates and sprite every 3
frames. The max is 100, where if it tries to add more, the oldest one gets removed.
When you exit playtesting, the saved positions appear one at a time, and you can use the Z key to speed it up.
[Here's a video of them in action.](https://o.lol-sa.me/4H21zCv.mp4)
2.2 and earlier had this god-awful thing where it put the closing tag of
an edentity onto the next line, and then kept the indentation the same.
This requires parsing the XML in an extremely specific way (i.e.
ignoring the whitespace) so the newline and indentation isn't taken as
part of the actual contents of the tag.
2.3 removed this awful whitespace entirely to make it easier on parsers.
When I tested #270, I tested against a 2.3 re-save of Dimension Open and
diffed the two, because I thought testing against the original version
of the level would result in a bunch of noise I didn't want due to the
whitespace change. Well, I did exactly what I intended, and ended up
ignoring the whitespace change so much that levels saved in this stupid
format ended up getting broken.
Luckily, we can just tell TinyXML-2 to parse a document exactly like how
TinyXML-1 would've parsed it, by supplying the COLLAPSE_WHITESPACE enum
to it (by default it's on PRESERVE_WHITESPACE).
This removes the TinyXML source files, removes it from CMakeLists.txt,
removes all the includes, and removes the functions
FILESYSTEM_saveTiXmlDocument() and FILESYSTEM_loadTiXmlDocument() (use
FILESYSTEM_saveTiXml2Document() and FILESYSTEM_loadTiXml2Document()
instead).
Additionally I've cleaned up the tinyxml2.h include in FileSystemUtils.h
so that it doesn't actually include tinyxml2.h unnecessarily, meaning a
change to TinyXML2 shouldn't rebuild all files that include
FileSystemUtils.h.
Seems a bit wasteful to do the whole "parse the XML document thing"
instead of a simple file check. It doesn't even fail if the XML document
is invalid, but whatever.
Ok, so it was a bit of a struggle at first figuring out the new API, but
honestly it wasn't so bad in the end.
I made a copy of my old unlock.vvv before testing this, and checking
with `diff` the only difference is the new `encoding="UTF-8"` in the XML
declaration, which isn't a bad thing.
Surprisingly, I only had to change some names and stuff around at the
top of the function. The rest of the function could be left untouched
and it worked fine.
Some of the file was indented with two spaces and the rest indented with
tabs. It feels like two different people worked on the file, one more
than the other. Since most of it uses two spaces, I'll just replace the
tabs with two spaces.
This is to respect the fact that the top half of the file is indented
with spaces, while the bottom half is indented with tabs.
Graphics::reloadresources() is on the bottom half.
The previous way manually concatenated the first 7 characters of the
string together (and had an std::min() calculation). The new way instead
does std::string::substr(), which is much more snappy.
The actual unindent is done in a separate commit to minimize noise,
because diffs are terrible at clearly conveying unindents (it should put
all the minus lines together and all the plus lines together, too).
The entirety of the rest of scriptclass::loadcustom() is encased in a
block that first checks if the script with the name even exists. Instead
of indenting the rest of the function, just invert the check and reduce
indentation level.
This commit refactors custom level scripts to no longer be stored in one
giant vector containing not only every single script name, but every
single script's contents as well. More specifically,
scriptclass::customscript has been converted to an std::vector<Script>
scriptclass::customscripts (note the extra S), and a Script is just a
struct with an std::string name and std::vector<std::string> contents.
This is an improvement in both performance and maintainability. The game
no longer has to look through script contents in case they're actually
script names, and then manually extract the script contents from there.
Instead, all it has to do is look for script names only. And the
contents are provided for free. This results in a performance gain.
Also, the old system resulted in lots of boilerplate everywhere anytime
scripts had to be handled or parsed. Now, the boilerplate is only done
when saving or loading a custom level. This makes code quality much,
much better.
To be sure I didn't actually change anything, I tested by first saving
Dimension Open in current 2.3 (because current 2.3 gets rid of the
awful edentity whitespace), and then resaved it on this patch. There is
absolutely no difference between the current-2.3-resave and
this-patch-resave.
This resulted in two bugs:
1. Custom assets would not be unmounted when quitting to the menu.
2. Custom assets would be unmounted when playtesting a level.
The solution is to unmount assets in Game::quittomenu() instead.
Main game would retain custom level assets, now fixed. Also, custom fonts load properly. Finally, levels can be stored as a zip and placed in the levels folder, with the .vvvvvv file at the root of the zip and custom asset folders (graphics, sounds etc) also at the root.
Previously, the editor would always say it saved or loaded a level,
even if it was not successful. For example, because a file to load does
not exist, a file to save has illegal characters in its name or the
name is too long to be stored. Now failure is reported. Also, when
quitting the editor and saving before quitting is unsuccessful, the
editor will abort quitting.
I think it's a bit silly to always include the Steam and GOG APIs
whenever we build VVVVVV, since the only time they'll ever be used is in
a live build and not a dev build.
So now Steam and GOG are disabled by default. If you want them, you'll
need to add -DSTEAM=ON or -DGOG=ON respectively at CMake time. They're
also both automatically enabled for release builds.
This commit adds bounds checks to those commands in case say()/reply()
asks for more lines than there are left in the all-script-lines buffer
(not just the current script, so in order for it to segfault your script
has to be last in the all-script-lines vector), and in case text() asks
for more lines than there are in the rest of the rest of the parsed
internal script.
For some reason, scriptclass::loadcustom() sometimes refers to
`customscript` as `script.customscript` instead of `customscript`. The
`script.` is unnecessary.
Although it's not an issue, this should minimize the stack footprint of
calling scriptclass::load(), especially if it goes down to calling
scriptclass::loadcustom() or scriptclass::loadother().
Otherwise, it would end up indexing a vector out-of-bounds, which is UB
and causes a segfault, too. This is used in some constructs like
say(-1)
<internal command>
where the text contents don't matter, only that a text box shows up.
The `speak` command is the exact same as the `speak_active` command,
except without one line of code. So instead of copy-pasting the entire
thing, it's better to just combine them into the same chunk of code.
Instead each line is now held in a const char* array, like it should be.
This results in less work for the compiler, especially with
optimization, since every time the compiler encounters a constant
argument in a function, it has to go off and locate a place to put it.
But if we're upfront and say, hey, here's all the strings we ever need
conveniently packaged into one place, it'll be much more cooperative.
I have the feeling that none of the devs understood what extern did, and
they kind of just sprinkled it everywhere until things started working.
But like all other classes, it should just be one line in the class's
respective header file, and shouldn't be so messy.
When the game loads a room in a custom level, previously it would load
the tilemap of that room into ed.swapmap, and then mapclass::loadlevel()
would manually go through each element in ed.swapmap to set each tile in
`contents`. Why do that, when you can just return the vector from
editorclass::loadlevel() and set it directly? ed.swapmap is really
unnecessary.
It's a bit bad for the compiler if you have lots of function calls with
hardcoded strings in them, because every time the compiler encounters
one, it has to go out of its way to find a dedicated storage location
for the string, which is really inefficient. And it does this
inefficient thing every single time.
There's not much of an impact compiling these lists, but I at least want
to encourage this sort of code style, instead of the push_back(string)
style, in case we ever need a hardcoded array of things later.
Resetting game.activeactivity fixes triggering Undefined Behavior if you
quit to the menu while standing inside an activity zone, and then
re-entered the game. Resetting game.act_fade also fixes the activity
zone prompt fadeout that happens once the above segfault is fixed.
Don't worry, other activity-zone like variables such as teleporter
prompts and trophy text are already covered. obj.trophytext is reset in
scriptclass::hardreset(), too, and game.readytotele is reset every time
mapclass::gotoroom() is called, and mapclass::gotoroom() is called every
time a gamemode is started.
For some reason, the only way to get a cyan crewmate is by cycling
through an already-existing crewmate by keeping left-clicking on it.
This is because when you cycle through crewmate colors, the allowed
colors are 0-5, but when you place down a crewmate, it picks a random
color from 1-5, which seems to be a bit consistent.
So placing and cycling a crewmate now use the same color ranges.
Blackout mode doesn't work properly, because the game doesn't actually
black out the screen, it merely stops drawing things. Oh and only some
things at that, some other things are still drawn. This results in a
freeze-frame effect, which is apparently fixed in the Switch version.
Custom levels utilize this freeze-frame effect, so it's a bit annoying
that the "Game paused" screen draws on top of it and makes it so that
the freeze-frame freezes on "Game paused" until blackout is turned off.
So I'm making it so that "Game paused" doesn't get drawn in blackout
mode. However it will still do graphics.render() because otherwise it'll
just result in a black screen.
This fixes being able to trigger Undefined Behavior by pressing R when
not in-bounds in the Outside Dimension VVVVVV map, usually when you're
falling upwards towards Game Complete.
I also put bounds checks on normal roomdeaths for good measure. You'll
never know when you need it.
Instead of using somewhat-obtuse for-loops to initialize or reset these
vectors, it takes up less lines of code and is clearer if we use
std::vector::resize() and std::vector::clear() instead.
Since translucent roomname backgrounds were introduced in
TerryCavanagh/VVVVVV#122, it exposes one glaring flaw with the game that
until now has been kept hidden: in rooms with room names, the game
cheapens out with the tile data and doesn't have a 30th row, because the
room name would hide the missing row. As a result, rooms with room names
have 29 rows instead of 30 to fill up the entire screen. And it looks
really weird when there's nothing but empty space behind the translucent
room name background.
To remedy this, I added one row to each room with a room name in the level.
First, I had to filter out all the rooms with no room names. However, that's
actually all contained in Otherlevel.cpp, the Overworld, which contains 221
rooms (8 of which are the Secret Lab, 6 more of which are the Ship, so 207 are
the actual Overworld, right? Wrong, 2 of those Overworld no-roomname rooms are
in the Lab, so there are actually 205 Overworld rooms). The remaining level
data files all contain rooms with room names.
But the process wasn't that easy. I noticed a while ago that each room
contains 29 `tmap.push_back()`s, one for each row of the room, and each row is
simply a string containing the 40 tiles for that row, concatenated with
commas.
However, I decided to actually check my intuition by doing a grep on each
level file and counting the number of results, for example `grep 'push_back'
Labclass.cpp | wc -l`. Whatever number comes out should be divisible by 29.
That particular grep on Labclass.cpp returns 1306, which divided by 29 is 45
with a remainder of 1.
So what does that mean? Does that mean there's 45 rooms each, and 1 leftover
row? Well, not exactly. The extra row comes from the fact that Outer Space has
30 rows instead of 29. Outer Space is the room that comes up when the game
finds a room is non-existent, which shouldn't happen with a properly-working
game, except in Outside Dimension VVVVVV. In fact, each level file has their
own Outer Space, and every single Outer Space also has 30 rooms. So really,
this means there are 44 rooms in the Lab and one Outer Space room. (Well, in
reality, there are 46 rooms in the Lab, because 2 of them use the Outside
tileset but have no room names, so they're stored in Otherlevel.cpp instead.)
We find the same result for the Warp Zone. `grep 'push_back' WarpClass.cpp |
wc -l` returns 697, which is 24 remainder 1, meaning 23 rooms of 29 rows and 1
room of 30 rows, which corresponds with 23 rooms in the Warp Zone and one
Outer Space room.
However, Outside Dimension VVVVVV + Tower Hallways and Space Station 1 and 2
are both odd curiosities. Finalclass.cpp contains Outside Dimension VVVVVV,
(which is Intermission 1 and 2 and the Final Level), but also the Tower
Hallway rooms, i.e. the auxiliary Tower rooms that are not a part of the main
tower. Spacestation2.cpp contains both Space Station 1 and 2, so don't be
deceived by the name.
`grep 'push_back' Finalclass.cpp | wc -l` returns 1597, which is actually 55
remainder 2. So... are there two rooms with 30 rows? Yes, in fact, The
Gravitron and Outer Space both contain 30 rows. So there are actually 55 rooms
stored in Finalclass.cpp (not including the minitowers Panic Room and The
Final Challenge), 54 rooms of actual level data and one Outer Space room, and
breaking down the 54 rooms even further, 51 of them are actually in Outside
Dimension VVVVVV and 3 of them are Tower Hallways. Of the 51 Outside Dimension
VVVVVV rooms, 14 of those are Intermission 1, 4 of them are Intermission 2,
and the rest of the 33 rooms are the Final Level (again, not including the
minitowers).
`grep 'push_back' Spacestation2.cpp | wc -l` returns 2148, which is 74
remainder 2. Are there two rooms with 30 rows again? No; one of those counted
2148 rows is a false-positive, because there's an if-else in Prize for the
Reckless that replaces the row with spikes with a row without spikes if you
are in a time trial or in No Death Mode. So there's 73 rooms in Space Station
1 and 2, and one Outer Space room.
With all this in mind, I decided to duplicate the current last row of each
room, the 29th row, to add a 30th row. However, I wasn't going to do this
automatically! But neither was I going to write some kludge-y code to parse
each nightmare of a level file and duplicate the rows that way.
Enter: Vim macros! (Er, well, actually, I use Neovim.) I first did
`/push_back`, so that pressing `n` would keep going to the next `push_back` in
the file. Then I went to the 29th row of the first room in the file, did a
`Yp`, and then started my macro with `qq`. The macro went like this: `30nYp`,
which is simply going to the 29th row of the next room over and duplicating
it. And that's all there was to it. However, I had to make sure that (1) my
cursor was before the `push_back` on the line of the 29th row of the room, and
(2) that I didn't skip rooms, both of which were problems I encountered when
pressing Ctrl+Z a given invocation of the macro (the Ctrl+Z is just a
metaphor, you actually undo by typing `u` in Vim). And also I had to make sure
to be careful around the extra lines of `push_back`s in Prize for the Reckless
and The Gravitron, and make sure I didn't run past the end of the file and
loop back around. Thankfully, all Outer Space rooms are at the end of each
file.
But first, I had to increase the number of rows drawn in Graphics.cpp by 1 in
order to compensate for this, and do the same when reading the tile data in
Map.cpp. I had to change fillcontent(), drawmap(), drawfinalmap(),
drawtowermap(), and drawtowermap_nobackground(). Funnily enough, the tower
functions already used 30 rows, but I guess it's an off-by-one due to the
camera scrolling, so they now draw 31 rows each.
Then, I went in-game to make sure that the row behind each room name looked
fine. I checked EVERY single room with a room name. I turned on invincibility
mode and added a temporary line to hardreset() that always turned on
game.nocutscenes for a smoother playtesting experience. And to make sure that
rooms which have entirely empty bottom rows actually still have 30 rows,
instead of having 29 and the game assuming that the 30th row was empty
(because that sounds like it could lead to Undefined Behavior), I added this
temporary debugging line to the start of mapclass::fillcontent():
printf("(%i,%i) has %i rows\n", game.roomx, game.roomy, (int) tmap.size());
Everywhere I checked - and I made sure to check all rooms - every room had 30
rows and not 29 rows.
Unfortunately, some rooms simply couldn't be left alone with their 29th row
duplicated and had to be manually edited. This was because the 29th row would
contain some edge tiles because the player would be able to walk somewhere on
the 28th, 27th, and 26th rows, and if you duplicated said edge tiles behind
the room name, it would look bad.
Here's a list of rooms whose 30th rows I had to manually edit:
- Comms Relay
- The Yes Men
- Stop and Reflect
- They Call Him Flipper
- Double-slit Experiment
- Square Root
- Brought to you by the letter G
- The Bernoulli Principle
- Purest Unobtainium
- I Smell Ozone
- Conveying a New Idea
- Upstream Downstream
- Give Me A V
- $eeing Dollar $ign$
- Doing Things The Hard Way
- Very Good
- Must I Do Everything For You?
- Now Stay Close To Me...
- ...But Not Too Close
- ...Not as I Do
- Do Try To Keep Up
- Whee Sports
- As you like it
This is actually a strange case where it looked bad because of the 29th
row, instead of the 30th row, and I had to change the 29th row instead of
the 30th row to fix it.
- Maze With No Entrance
- Ascending and Descending
- Mind The Gap
Same strange case as "As you like it" (it's the 29th row I had to change
that was the problem, not the 30th).
- 1950 Silverstone Grand V
- The Villi People
I found that Panic Room and The Final Challenge also looked strange behind the
roomname background, but I can't do much about either because towers' tile
data wrap around at the top and bottom, and if I added another row to either
it would be visible above the room name.
I've considered updating the development editors with these new level tiles,
but I decided against it as the development editors are already pretty
outdated anyway.
This didn't cause any issues on the old push_back(string) system, but it
causes a huge alignment issue now. Don't know why these 7 zeroes are
here, but I'm fixing them.
It's a bit inconsistent how every time you toggle flip mode, it does
this flashing and shaking (and different SFX), regardless of whether or
not you're turning it on or off. To be more consistent with what happens
when you toggle screen effects, only turning on Flip Mode should do the
flashing/shaking/game-saved sound, and turning it off should just play
the Viridian squeak.
It's always personally irked me that the only time you get a Viridian
squeak when pressing ACTION to start fading out and going in-game is
when you start playing a custom level. And that's only if you don't have
a quicksave.
To make things more consistent (and to add more polish to the game), I
made sure there was a music.playef(11) every time game.mainmenu gets
set. I also made sure that this doesn't result in double-squeaking, i.e.
music.playef(11) being called twice, which can be very loud and
ear-hurting.
If you started a new game while having had a save (meaning you selectedd
"new game" while it wasn't in the same position as "continue"), then
saved and quit, your cursor will now end up at "continue" instead of
"new game". (If you didn't save, then your cursor would be out-of-bounds
and end up at position 0 anyway.)
For some reason (probably a copy-paste error), this XML tag gets atoi()
called on it before being assigned to Game::hardestroom. And only when
loading a quicksave, at that.
This would result in Game::hardestroom being set to an empty string,
which if you kept until Game Complete, would end up rendering as a
single null byte (if you even have a font face for said null byte).
I'm not sure how this error compiles in the first place, but whatever.
Whenever I compile with -O2, GCC gives me a warning that the return
value of fread() is being ignored. Which is a fair point, it means that
we're not doing proper error handling if something goes wrong. But I'm
also going to check the return value of fwrite() for good measure.
I believe that checking that this number is not equal to length is the
way to catch all errors and output an error message accordingly. I
didn't use ferror() or feof() mostly because I think it takes up too
much code. Also an error from fwrite() only says "Warning" because I
don't think there's much we can do if we don't fully write all bytes to
the intended file.
Previously, it was used to parse 30 strings whenever loading a room. But
now it's no longer used since we just assign the tilemap to the vector
directly.
They are now stored in const int arrays instead. Except for the Prize
for the Reckless room, which I made sure had its spikes removed in No
Death Mode and the Time Trial.
Instead, they're all stored in a constant int array.
I made sure The Gravitron still has 30 rows just like Outer Space,
though I don't think it matters.
Previously:
- Linux: xdg-open
- Everything else: open
Now:
- macOS and Haiku: open
- Everything else: xdg-open
This is all according to a comment by leo60228 in PR #203.
This change makes it so that in in-editor playtesting, the code to
handle pressing ENTER on activity zones is the same code to handle
pressing ENTER on activity zones in custommodeforreal.
This removes the need to copy-paste code and adapt it to in-editor
playtesting. And thus, this fixes an editor artifact where you can press
ENTER on activity zones while doing the death animation, even though you
can't do that when you're playing custom levels normally.
Since this is at the start of maprender(), the text boxes drawn by this
function will get hidden behind everything else being drawn on top of
it. Thus, it'll only result in a glitchy rendering where the text boxes
at the very top of the screen will be rendered in the roomname space of
the map screen.
To fix this rendering glitch, just remove the drawgui(). It's useless
anyway since it's being drawn behind everything else, so no need to have
it here.
We need to replace an "or" with an "and".
My best guess for this oversight happening was because of the weird
ordering. The code originally did "temp < 30" first and "temp > -30"
second instead of the other way around. With the weird ordering, it
becomes more natural to insert an "or" instead of an "and". So I swapped
around the ordering just for good measure.
This is also fixed in the mobile version.
This fixes horizontal and vertical warp backgrounds not resetting, and
also a bunch of other 1-frame glitches, most noticeably cutscene bars
and fadeouts.
This adds a new variable shouldreturntoeditor to Game to signal whether
or not it should return to editor at the end of the frame.
It's a bit annoying how vvvvvvman status is preserved between in-game
sessions, and the only thing reset is the color. This is annoying
because it means you have to close the game to reset vvvvvvman.
But now it'll be reset properly. I chose to put this reset code in
mapclass::resetplayer() instead of scriptclass::hardreset() because it
seemed like the more appropriate place. It's where all other properties
of the player are reset, after all.
This is to be extra safe and ensure that your hard-earned record isn't
lost at all.
In 2.2, the game didn't save your Super Gravitron record at all. It only
saved it if you closed the game by quitting to the title screen and
pressing ACTION on "quit game". You couldn't press Alt+F4, and you
couldn't press X, you had to do it that way, otherwise your record would
be lost.
In 2.3 right now, the game WILL save your unlock.vvv when you close the
game gracefully by any means, but this still means that if it doesn't
otherwise close gracefully (like, say, a crash), it won't save your
record. It feels like we shouldn't rely on this catch-all saving to save
Super Gravitron records.
Flag 67 seems to be a general-purpose Game Complete flag. It's used to
replace the CREW option with the SHIP option on the pause screen.
Unfortunately, it gets turned on too early during Game Complete. Right
when Viridian starts to teleport, you can bring up the pause screen and
select the SHIP option.
It will teleport you to the ship coordinates, but still keep you in
finalmode, and since the ship coordinates are at 102,111 (finalmode is
only around 46,54), you'll still be stuck in Outside Dimension VVVVVV,
and you've interrupted the Game Complete gamestate by switching to the
teleporting gamestate. Oh, and your checkpoint is set, too, so you can't
even press R. Oh and since there's no teleporter, and checkpoint setting
doesn't check the sentinel value, this results in Undefined Behavior,
too.
So this results in an in-game softlock. The only option you can do is
quit the game at this point.
To fix this issue, just move turning on flag 67 before the savetele() in
gamecompletelogic2().