So, I was staring at VVVVVV code one day, as I usually do, and I noticed
that warp lines had this curious code in entityclass::updateentities()
that set their statedelay to 2, and I thought, hm, maybe the pre-2.1
warp line bypass is caused by this statedelay. And, it doesn't seem like
this is the primary code used to detect if the player collides with warp
lines, the actual code is commented with "Rewritten system for mobile
update" and bolted-on in gamelogic() instead of properly being in
entityclass::entitycollisioncheck().
So, after getting tripped up on the misleading indentation of that
"Rewritten system" block, I removed the rewritten system, re-added
collision detection for rule 7 (horizontal warp lines), and after
checking the resulting behavior, it appears to be nearly identical to
that of warp lines in 2.0.
You see, if you use warp lines to flip up from the top of the screen
onto the bottom of the screen, close to the edge of the bottom of the
screen, Viridian's head will display on the top of the screen in 2.0. In
2.1 and later, this doesn't happen, confirming that my theory is
correct. I also performed warp line bypass multiple times in 2.0 and
with my restored code, and it is pretty much the exact same behavior.
So now, the pre-2.1 warp line bypass glitch has been re-enabled in
glitchrunner mode.
This misleading indentation makes it really easy to misanalyze this
block as only containing the statements up to obj.customwarplinecheck(),
when in reality it contains everything up to the modifying of
map.warpx/map.warpy. I have made this misanalysis just now in my attempt
to figure out the pre-2.1 warp line bypass glitch, and I don't like it.
So I'm fixing this indentation now.
So there's this trick that I recently discovered, since many script
commands don't initialize `i` it's possible to use them to manipulate
arbitrary entities by specifying their indice.
This means in 2.2 you can convert entities to pseudo-crewmates by
changing their rule to 6. Except in 2.3, this was fixed when I fixed the
command to work on flipped crewmates as well. So I'm restoring this
functionality, but I recognize the protection that my previous change to
the command did in preventing levels from destroying the player entity
by changing the player's rule to something nonzero, so instead of
removing the if-conditional entirely, I'm making it so that it will only
set the rule if the entity's rule isn't 0.
For some reason, GetSubSurface() and ApplyFilter() were hardcoding the
bits-per-pixel and/or mask arguments to SDL_CreateRGBSurface(). I've
made them simply re-use the bits-per-pixel and masks of the input
surfaces they operate on, but the bits-per-pixel should always be 32 and
masks should always go first-byte alpha, second-byte red, third-byte
green, fourth-byte blue.
The game usually runs on little-endian anyways, but even if it did run
on big-endian, it doesn't check endianness everywhere so these checks
are useless. Furthermore, the code should be written so that endianness
doesn't matter in the first place.
Neither of these were used anywhere, so to simplify the code and prevent
having potentially broken code that's never shown to be broken because
it's never tested, I'm removing these.
For some reason, there's some color-clamping code in this function
directly after already-existing color-clamping code. This code dates
back to 2.2. And also, there's a smaller lower-bound of -1 for the red
channel, despite the fact that this smaller value doesn't matter because
the red would get clamped to 0 by the first code anyway.
So even if this was put here for some strange reason, it doesn't matter
because it doesn't do anything anyway.
It's trivially easy to send the scripting system into an infinite loop
on the same frame (i.e. without any script delay in between, i.e. within
the same execution of script.run()). Just take a look at these two
scripts:
a:
iftrinkets(0,b)
#
b:
iftrinkets(0,a)
#
The hashes are to prevent the scripting system from parsing iftrinkets()
using the internal version instead of the simplified version, because
after doing a simplified iftrinkets(), the parser will (to oversimplify)
execute the last line of the script as internal.
Anyway, sending the game into an infinite loop like this will cause the
Not Responding dialog on Windows.
So to prevent this from happening, I've added an execution counter to
scriptclass::run(). If it gets too high, we're in an infinite loop and
so we stop running the script.
I noticed that if I have a large amount of entities in the room, the
game starts to freeze and one frame would take a very long time. I
identified an obvious cause of this, which is that the entity collision
checking in entityclass::entitycollisioncheck() is O(n²), n being the
number of entities in the room.
But it doesn't need to be O(n²). The only entities you need to check
against all other entities are the player and the supercrewmate. You
don't need to "test entity to entity" if 99% of the pairs of entities
you're checking don't involve the player or supercrewmate.
How do we make it O(n)? Well, just hoist the rule 0 and type 14 checks
out of the inner for-loop. That way, the inner for-loop won't be
unconditionally ran, meaning that in most cases it will always be O(n).
However, if you start having large amounts of duplicate player or
supercrewmate entities (I don't know why you would), it would start
approaching O(n²), but I feel like that's fair in that case. But most of
the time, it will be O(n).
So that's how collision checking is now O(n) instead.
There's not really any good reason to prevent this action during a fade
animation. That just makes the fade timer one more potential contributor
to a softlock.
I'm leaving the fademode conditional on the Time Trial quick restart,
though - removing it would mean being able to quick restart during a
fade-in, and thus being able to spam Enter over and over to keep
re-starting the fade animation, which looks goofy.
The hooks to bring up the map screen, pause screen, quit from Super
Gravitron, restart Time Trial, and commit suicide have now been hoisted
out of the for-loop that checked for a player entity. None of these
actions require a player entity, and there's no good reason to take away
your control from any of these actions, especially being able to quit to
the menu. The only actions inside the for-loop now are activating a
terminal and activating a teleporter, both of which require a player
entity to be standing in front of a terminal or teleporter, and both of
which have good reasons to be temporarily disabled.
This makes it so the fix for crewmates' drawframes being wrong for
1-frame is fixed for all crewmates regardless of when they get created.
Sure, crewmates created in mapclass::loadlevel() have their drawframes
fixed there, but for crewmates that get created from scripting (such as
Violet when gotorooming to the Ship teleporter room after Space Station
1), this fix doesn't apply to them. But now it does, and Violet will no
longer be facing the wrong way for 1 frame when teleporting to the Ship
teleporter room in the Space Station 1 Level Complete cutscene.
If the player is stuck in a wall (which shouldn't happen in the first
place), their sprite would always default to being flipped, even if they
were unflipped.
Being stuck in a wall is characterized by having both positive onfloor
and onground.
It's perfectly acceptable to have both warp lines and a warping
background in the same room. Many levels do this exact thing, I would
say at least 30 or so levels, many of them popular and played by many,
and this has never caused any issues at all.
All that having both warp lines and warp BG does is make it so the
warping of the warping background gets overriden by the warp lines, but
make it so the background is still a warp background. So in effect, you
can have a warp background without any warping. This is perfectly
defined behavior. Except, for whatever reason, it's unintentional, and
the editor tries to prevent you from doing it.
Key word being "tries". The code to prevent having both warp types is
bugged (at least when you change the warp BG. The check when you place
warp lines seems to be solid). It compares the p1 and p2 attributes of
warp lines to the x-coordinate and y-coordinate of the room, despite p1
and p2 having nothing to do with room coordinates. p1 is the type of the
warp line and should be treated as an enum, and p2 is the offset of the
warp line from the top/left of the screen. This results in this check
sometimes working if you're unlucky, but never actually working properly
most of the time. This means people can first place warp lines, and then
change the warp background later, to have both warp lines and a warp
background.
Having these checks just further complicates the code, makes it more
error-prone, and just inconveniences people when they want to do
something that's perfectly fine to do. So it's best if we just remove
these checks.
Fixes#402 (Violet appearing 1 frame after the Ship teleporter room
appears).
The root cause of this bug is due to the game loop order changes made
with the over-30-FPS patch. 2.2's game loop order was gameinput(),
gamerender(), then gamelogic(). In 2.3, gamerender() was moved to the end
as it required special code to render more than 30 frames a second. So
2.3's game loop order is gameinput(), gamelogic(), then gamerender().
In hindsight, I could have preserved the game loop order, but this would
require some more complex code in the game loop than what is there
currently. Fixing it now would fix rendering glitches such as this one
(along with being able to remove script.dontrunnextframe with the
two-frame-delay fix), but it could also introduce new rendering glitches
we don't yet know about. After discussing this in Discord DMs with
flibit, we agreed that the game loop order should be fixed in 2.4
instead.
When the game teleports you, gamelogic() runs script.teleport(). This
function will gotoroom to the teleporter destination, then it loads the
teleport script. Some teleport scripts (such as levelonecomplete, which
creates Violet) expect that their entities will be created, and more
generally that their script will be ran, on the same frame that the
gotoroom happens, i.e. by the time that the next gamerender() happens,
i.e. script.run() should be ran before the next gamerender() happens.
This would be true on the old game loop order, but with the new game
loop order, gamerender() gets ran directly after gamelogic() with no
script.run() in between.
To fix this, I did the same thing I did with the two-frame-delay fix
(#317), where I ran the script for that frame, but in order to prevent
running it twice I set script.dontrunnextframe to true.
This is a follow-up to #421.
The game would draw the activity zone if there was one active at all,
and would ignore game.act_fade. Normally this wouldn't be a problem, but
since #421, it's possible that there could be an active activity zone
but game.act_fade would still be 5. This would happen if you entered a
new activity zone while a cutscene was running.
To fix this, just make it so that the prompt gets drawn on its own and
only depends on the state of game.act_fade (or game.prev_act_fade), and
won't be unconditionally drawn if there's an active activity zone. As a
bonus, this de-duplicates the drawing of the activity prompt, so it's no
longer copy-pasted.
This fixes a bug where opening a script, then closing the script but not
exiting the script list, then opening a script again (it can be the same
script as before) would result in you being unable to type in the
script. You would have to close the script, then close the script list,
then re-open the script list in order to be able to type properly.
This was caused by the fact that key.enabletextentry() was being called
when the script list was opened, but key.disabletextentry() was being
called when closing a script. So the fix for this is to simply move the
key.enabletextentry() to its proper place, which is when the script is
being opened, and not when the script list is.
Some levels rely specifically on the fact that certain script boxes are
loaded using gamestates, instead of directly loading the script and
bypassing the gamestate system. Then weird things could happen. This
restores compatibility with those levels.
mapclass::twoframedelayfix() doesn't need to be updated because the
point of that function is to bypass the gamestate system entirely
anyways.
There's not really any need for it to be there. It gets called when the
Time Trial restarts, as restarting the Time Trial calls
script.startgamemode(), which calls script.hardreset() anyway.
Furthermore, since script.hardreset() is removed, we can also remove two
lines that are meant to work around the fact that everything gets reset,
which is now no longer the case.
Fixes#367.
Previously, it was assuming that the number of PPPPPP/MMMMMM tracks
would always be 16, since if that wasn't the case... then the game would
rudely and abruptly segfault when attempting to load the file. Huh.
But now that the game properly deals with invalid headers, it's possible
for the number of tracks to be 0. So I'll need to remove this
assumption.
It's possible that musicReadBlob.getIndex() could return the sentinel
value of -1 in case the header with that name is invalid, in which case
we should simply not do anything. Otherwise it'll lead to segfaults. I
opted to do the full bounds check just to be safe, too.
For further safety, I hardcoded the max number of headers, 128, less, so
128 is copy-pasted less and in the future if it needs to be changed
it'll only have to be changed in one place.
The binary blob shouldn't return an index if it ends up being invalid.
That could cause a whole lot of issues if musicclass ends up parsing the
resulting struct.
With all that said, though, musicclass doesn't check the -1 sentinel
value anyway, even though it should, but that'll be fixed later.
This fixes the bug where in glitchrunner mode, quitting to the menu
would always put you back at the play menu on the first option, instead
of the menu you entered the game from.
The problem is the script.hardreset() that gets called before the game
actually quits to the menu, so when Game::quittomenu() gets called to
quit to the menu, all the variables that keep track of whether you're in
a certain gamemode, such as game.insecretlab and map.custommode, all get
prematurely reset before that function can read them and put you back to
the correct menu.
The solution here is to simply reset only what's needed when quitting to
the menu. Specifically, in order for credits warp to work,
script.running needs to be set to false and all the text boxes need to
be removed. Text boxes need to be gone so the "- Press ACTION to advance
text -" prompt will stay up without a text box, enabling the player to
increment the gamestate at will by pressing ACTION, and the script needs
to stop running so further text boxes don't spawn in.
Fixes#389.
After looking at pull request #446, I got a bit annoyed that we have TWO
variables, key.textentrymode and ed.textentry, that we rolled ourselves
to track the state of something SDL already provides us a function to
easily query: SDL_IsTextInputActive(). We don't need to have either of
these two variables, and we shouldn't.
So that's what I do in this patch. Both variables have been axed in
favor of using this function, and I just made a wrapper out of it, named
key.textentry().
For bonus points, this gets rid of the ugly NO_CUSTOM_LEVELS and
NO_EDITOR ifdef in main.cpp, since text entry is enabled when entering
the script list and disabled when exiting it. This makes the code there
easier to read, too.
Furthermore, apparently key.textentrymode was initialized to *true*
instead of false... for whatever reason. But that's gone now, too.
Now, you'd think there wouldn't be any downside to using
SDL_IsTextInputActive(). After all, it's a function that SDL itself
provides, right?
Wrong. For whatever reason, it seems like text input is active *from the
start of the program*, meaning that what would happen is I would go into
the editor, and find that I can't move around nor place tiles nor
anything else. Then I would press Esc, and then suddenly become able to
do those things I wanted to do before.
I have no idea why the above happens, but all I can do is to just insert
an SDL_StopTextInput() immediately after the SDL_Init() in main.cpp. Of
course, I have to surround it with an SDL_IsTextInputActive() check to
make sure I don't do anything extraneous by stopping input when it's
already stopped.
All that pressing R does in No Death Mode is end your run. As a result,
it'll only be pressed by accident, so it's better to just disable it
instead.
It's not even useful to quick-restart, because it's faster to quit and
go through the menu again than it is to wait through the Game Over
screen.
Additionally, I removed the `game.deathseq<=0` conditional because it's
unnecessary due to the if-statement already being inside a
`game.deathseq == -1` conditional.
strcat()s and strcpy()s have been replaced with SDL_snprintf() where
possible, to clearly convey the intent of just building a string that
looks a certain way, instead of spanning it out over multiple lines.
Where there's not really a good way to avoid strcat()/strcpy() (e.g. in
PLATFORM_getOSDirectory()), they will at least be replaced with
SDL_strlcat() and SDL_strlcpy(), which are safer functions and are less
likely to have issues with null termination.
I decided not to bother with PLATFORM_migrateSaveData(), because it's
going to be axed in 2.4 anyways.
There's no need to call a string function and have function call
overhead if you remember how C strings work: they have a null
terminator. So if the first char in a string is a null terminator, then
the string is completely empty. So you don't need to call that function.
The previous check by mwpenny had a few issues:
(a) It was completely overcomplicated for no good reason, and was
basically a Rube Goldberg machine. The original check was...
(1) Creating an std::string of the last char of 'output'...
(2) ...except instead of using the normal std::string constructor, it
was using the one where you pass in a number and a char to create
a string that's just that char repeated N times... except this
was only used to create a 1-length string.
(3) Converted that std::string to a C string.
(4) Then passed it to strcmp(), despite the string at this point
being only one byte and you could just compare the char values
directly.
The original check could've just been:
output[SDL_strlen(output) - 1] == *pathSep
(b) Use of libc strcmp() and strlen() instead of SDL_strcmp() and
SDL_strlen().
Now, actually, PHYSFS_getDirSeparator() happens to be a char array and
not a single char, so mwpenny was going in the right direction by using
strcmp() after all. Except it doesn't seem like he thought about the
fact that PHYSFS_getDirSeparator() could be multiple bytes instead of
one, and so he ended up making the first argument to strcmp() always be
a one-byte char array.
So there's issue (c), which is that it assumes the path separator is one
byte instead of multiple.
This commit fixes all of these issues with the trailing path separator
check.
If necessary, the caller can provide a fallback to be returned in case
the input given isn't a valid integer, instead of having to duplicate
the is_number() check.
This is simply a wrapper function around SDL_atoi(), because SDL_atoi()
could call libc atoi(), and whether or not invalid input passed into the
libc atoi() is undefined behavior depends on the given libc. So it's
safer to just add a wrapper function that checks that the string given
isn't bogus.