The onroof/onground attributes are used to determine if the player is
standing on a surface and is eligible to flip. Most notably, it is an
integer and not a boolean, and it starts at 2, giving the player 2
frames to edge-flip, i.e. they can still flip 2 frames after walking off
an edge.
However, these attributes are unnecessarily reassigned in
movingplatformfix() (which is the function that deals exclusively with
vertically-moving platforms; horizontal moving platforms get their own
hormovingplatformfix()). Whoever wrote this misunderstood what
onroof/onground meant; they thought that they were booleans, and so set
them to true, instead of the proper value of 2. This ends up setting
onroof/onground to 1 instead of 2, causing a discrepancy with vertical
moving platforms and the rest of the surfaces in the game.
The bigger mistake here is duplicating code that never needed to be
duplicated. The onroof/onground assignment in gamelogic() works
perfectly fine for vertical moving platforms. Indeed, after testing it
with libTAS, I can confirm that removing the duplicate assignments
restores being able to edge-flip off of moving platforms with 2 frames
of leeway, instead of only 1 frame. It also doesn't change how long it
takes for the onroof/onground to get set when the player is recognized
as standing on a vertically-moving platform, either.
And so, it's better to not duplicate this code, because when you
duplicate it you run the risk of making a mistake, as I just
demonstrated.
By "unnecessary qualifiers to self", I mean something like using the
'game.' qualifier for a variable on the Game class when you're inside a
function on the Game class itself. This patch is to enforce consistency
as most of the code doesn't have these unnecessary qualifiers.
To prevent further unnecessary qualifiers to self, I made it so the
extern in each header file can be omitted by using a define. That way,
if someone writes something referring to 'game.' on a Game function,
there will be a compile error.
However, if you really need to have a reference to the global name, and
you're within the same .cpp file as the implementation of that object,
you can just do the extern at the function-level. A good example of this
is editorinput()/editorrender()/editorlogic() in editor.cpp. In my
opinion, they should probably be split off into their own separate file
because editor.cpp is getting way too big, but this will do for now.
This is a refactor that simply moves all temporary variables off of
entityclass, and makes it so they are no longer global variables. This
makes the resulting code easier to understand as it is less entangled
with global state.
These attributes were:
- colpoint1
- colpoint2
- tempx
- tempy
- tempw
- temph
- temp
- temp2
- tpx1
- tpy1
- tpx2
- tpy2
- temprect
- temprect2
- x (actually unused)
- dx
- dy
- dr
- px
- py
- linetemp
- activetrigger
- skipblocks
- skipdirblocks
Most of these attributes were assigned before any of the times they were
used, so it's easy to prove that ungloballing them won't change any
behaviors. However, dx, dy, dr, and skipblocks are a bit more tricky to
analyze. They relate to blocks, with dx, dy, and dr more specifically
relating to one-way tiles. So after some testing with the quirks of
one-way tiles, it seems that the jankiness of one-way tiles haven't
changed at all, either.
Unfortunately, the attribute k is clearly used without being assigned
beforehand, so I can't move it off of entityclass. It's the same story
with the attribute k that Graphics has, too.
This prevents users from being confused whenever they type a pipe in the
script editor, then save the level and load it again and see their
script lines unexpectedly splitting in two. Now if you attempt to type a
pipe, it simply won't happen at all.
Fixes#379.
It's possible that SDL_atoi() could call the libc atoi(), and if a
string is provided that's too large to fit into an integer, then that
would result in undefined behavior. To avoid this, use SDL_strtol()
instead.
Instead of copying to a temporary string, just use SDL_strncmp(). Also,
I checked the blame, and apparently I committed the line that used
strcmp() instead of SDL_strcmp(), for whatever reason. But that's fixed
now.
For some reason, the variable `k` is on entityclass and gets mutated in
createentity() and createblock(). Then updateentities() uses it without
checking if it's valid, because either `k` or the size of `entities`
could have changed in the meantime. To fix any potential undefined
behavior, these bounds checks should be added.
This fixes a bug where font_positions wouldn't get cleared after exiting
a custom level that had a font.txt if it didn't exist in the default
graphics, leading to messed-up-looking font rendering.
This bug was originally discovered by Ally.
You're intended to rescue Violet first, and not second, third, or
fourth, and especially not last.
If you rescue her second, third, or fourth, your crewmate progress will
be reset, but you won't be able to re-rescue them again. This is because
Vitellary, Verdigris, Victoria, and Vermilion will be temporarily marked
as rescued during the `bigopenworld` cutscene, so duplicate versions of
them don't spawn during the cutscene, and then will be marked as missing
later to undo it.
This first issue can be trivially fixed by simply toggling flags to
prevent duplicates of them from spawning during the cutscene instead of
fiddling with their rescue statuses.
However, there is still another issue. If you rescue Violet last, then
you won't be warped to the Final Level, meaning you can't properly
complete the game. This can be fixed by adding a `crewrescued() == 6`
check to the Space Station 1 Level Complete cutscene. There is
additionally a temporary unrescuing of Violet so she doesn't get
duplicated during the `bigopenworld` cutscene, and I've had to move that
to the start of the `bigopenworld` and `bigopenworldskip` scripts,
otherwise the `crewrescued() == 6` check won't work properly.
I haven't added hooks for Intermission 1 or 2 because you're not really
meant to play the intermissions with Violet (but you probably could
anyway, there'd just be no dialogue).
Oh, and the pre-Final Level cutscene expects the player to already be
hidden before it starts playing, but if you rescue Violet last the
player is still visible, so I've fixed that. But there still ends up
being two Violets, so I'll probably replace it with a special cutscene
later that's not so nonsensical.
I have no idea why neither conveyors and moving and disappearing
platforms rendered in the editor or in-game use
Graphics::drawentcolours(), but this needs to be bounds-checked just as
I did for the in-game rendering function.
The entity getters I'm referring to are entityclass::getscm(),
entityclass::getlineat(), entityclass::getcrewman(), and
entityclass::getcustomcrewman().
Even though the player should always exist, and the player should always
be indice 0, I wouldn't want to make that assumption. I've been wrong
before.
Also, these functions returning 0 lull you into a false sense of
security. If you assume that commands using these functions are fine,
you'll forget about the fact that `i` in those commands could be
potentially anything, given an invalid argument. In fact, it's possible
to index createactivityzone(), flipgravity(), and customposition()
out-of-bounds by setting `i` to anything! Well, WAS possible. I fixed it
so now they can't.
Furthermore, in the game.scmmoveme block in gamelogic(), obj.getplayer()
wasn't even checked, even though it's been checked in all other places.
I only caught it just now because I wanted to bounds-check all usages of
obj.getscm(), too, and that game.scmmove block also used obj.getscm()
without bounds-checking it as well.
When this is done, there is potential for a mistake to occur when
writing out the bounds check, which is eliminated when using the macro
instead. Luckily, this doesn't seem to have happened, but what's even
worse is I hardcoded 400 instead of using SDL_arraysize(ed.level), so if
the size of ed.level the bounds checks would all be wrong, which
wouldn't be good. But that's fixed now, too.
This is because if they are manually written out, they are more likely
to contain mistakes.
In fact, after further review, there are several functions with
incorrect manually-written bounds checks:
* entityclass::entitycollide()
* entityclass::removeentity()
* entityclass::removeblock()
* entityclass::copylinecross()
* entityclass::revertlinecross()
All of those functions forgot to do 'greater than or equal to' instead
of 'greater than' when comparing against the size of the vector. So they
were erroneous. But they are now fixed.
It's better to do INBOUNDS_VEC(i, obj.entities) instead of 'i > -1'.
'i > -1' is used in cases like obj.getplayer(), which COULD return a
sentinel value of -1 and so correct code will have to check that value.
However, I am now of the opinion that INBOUNDS_VEC() should be used and
isn't unnecessary.
Consider the case of the face() script command: it's not enough to check
i > -1, you should read the routine carefully. Because if you look
closely, you'll see that it's not guaranteed that 'i' will be initialized
at all in that command. Indeed, if you call face() with invalid
arguments, it won't be. And so, 'i' could be something like 215, and
that would index out-of-bounds, and that wouldn't be good. Therefore,
it's better to have the full bounds check instead of checking only one
bounds. Many commands are like this, after some searching I can also
name position(), changemood(), changetile(), changegravity(), etc.
It also makes the code more explicit. Now you don't have to wonder what
-1 means or why it's being checked, you can just read the 'INBOUNDS' and
go "oh, that checks if it's actually inbounds or not".
For some reason it called obj.getplayer() and did nothing with the
result. Weird. But it does say "Test script for space station" above.
Removing this fixes an 'unused variable' warning.
With the scope of these variables reduced, it makes analyzing this
function easier, as you can now clearly see all temporary variables are
actually initialized before they're used.
Since there's an INBOUNDS_ARR() macro, it's much better to specify the
macro for the vector is a macro for the vector, to avoid confusion.
All usages of this macro have been renamed accordingly.
Stuck prevention (pushing the player/supercrewmate out if they are
inside a wall) has been factored out into its own function, so it's no
longer copy-pasted but slightly tweaked just for the supercrewmate.
Instead of having two separate functions to move entities along vertical
moving platforms, one for the player and one for the supercrewmate, they
have been consolidated into one function.
In-level, they were made to be gray in #323. The editor does not reflect this however; they're still shown as
green. For the same reasons in #323, this adds special cases to draw the entities as gray.
Closes#372.
Also, I changed my name in contributors.txt to be my username as I didn't feel comfortable with it being my name.
Co-authored-by: Misa <ness.of.onett.earthbound@gmail.com>
The game has four different functions for the same XML schema:
Game::loadtele(), Game::savetele(), Game::loadquick(), and
Game::savequick(). This essentially means one XML schema has been
copy-pasted three different times.
I can at least trim that number down to being copy-pasted only one time
by de-duplicating the reading and writing part. So both Game::loadtele()
and Game::loadquick() now use Game::readmaingamesave(), and
Game::savetele() and Game::savequick() now use
Game::writemaingamesave().
This will make it take less work to add XML forwards compatibility
(#373).
Due to #464, standing inside a gravity line during a gotoroom that
occurs every frame will end up with the gravity line being gray instead
of being white. To temporarily fix this (until #464 is properly fixed),
I decided to add some kludge that colors it white if its onentity is 1.
I tested this patch with gravity lines in both constant-gotoroom and
normal environments, and it seems to be fine for both.
In 2.0, 2.1, and 2.2, calling flipgravity() on an entity that wasn't
rule 6 would change it to rule 7. In 2.3 currently, doing this will only
change it to rule 7 if it's already rule 6, starting with the
introduction of the change where if an entity was rule 7 it would be
changed to rule 6.
The crewmate conversion trick has been restored, but converting an
entity to a crewmate will change its rule to 6, not 7 like in pre-2.3.
If you want it to be changed to rule 7 instead of 6, you'd have to call
flipgravity() twice in 2.3 and only once in pre-2.3, which would make
maintaining compatibility between versions a bit harder.
So to fix this, I'm inverting it so that if you call flipgravity() on an
entity that isn't rule 7, it will be converted to rule 7, and only if
it's rule 7 will it be converted to rule 6.
This is a followup to b7cf6855b0 and
10ed0058fd.
In 2.2, if you had a duplicate player entity, there'd be no way to get
rid of it. Except for the recently-discovered Arbitrary Entity
Manipulation glitch, where you set `i` to the indice of the entity and
call flipgravity() on it, turning its rule to 7 and making it no longer
a player entity.
However, I patched this useful mechanic out when I made it so that you'd
no longer be able to convert entities with rule 0 to rule 6
(10ed0058fd, upheld in
b7cf6855b0), because doing so would mean
being able to softlock the game by not having any player entity.
So, in this patch, I'm making it so that you CAN convert duplicate
player entities to crewmates (and thus basically destroy them), but you
can't do that to the TRUE player entity (i.e. the first entity indice
that has rule 0, which is basically always indice 0).
This patch fixes a regression caused by commit
6b1a7ebce6.
When you spawn a crewmate with an invalid color, by doing something like
`createentity(100,100,18,-1,0)` (here the color is -1, which is
invalid), a white crewmate with the color of solid white (255, 255, 255)
would appear.
That is, until AllyTally came along and committed commit
6b1a7ebce6 (Make "[Press ENTER to
return to editor]" fade out after a bit) (PR #158). Then after that
commit, it would seem like the crewmate didn't appear, but in reality
they were just invisible, because they had an invisible color.
As part of Ally's changes, to properly support drawing text with a
certain amount of alpha, she made BlitSurfaceColoured() account for the
alpha of the color given instead of only caring about the RGB of the
color, discarding the alpha, and using the alpha of the surface it was
drawing instead. This effectively made it so the alpha of whatever it
was drawing would be 255 all the time, except for if you had custom
textures and your custom textures had translucent pixels.
However, the default color set by Graphics::setcol() if you didn't
provide a valid color index was 0xFFFFFF. Which is only (255, 255, 255)
but ends up having an alpha value of 0 (because it's actually
0x00FFFFFF). And all colors drawn with alpha 0 end up being drawn with
alpha 0 after 6b1a7ebce6. So
invalid-colored entities will end up being invisible.
To fix this, I just decided to add alpha to the default value instead.
In addition, I used getRGB() to be consistent with all the other colors
in the function.
This is a regression from 25779606b4
(PR #74).
On the one hand, I should've thought this through carefully when
implementing the fix for the lower-score overwrite bug. On the other
hand, flibit didn't seem to notice either. And on the third hand, no one
else seems to have noticed, when they have had over 8 months to do so.
Not even the person who originally reported the lower-score overwrite
bug and also reported other bugs I hadn't noticed (e.g. the "You have
rescued a crewmate!" in Flip Mode) noticed this bug, which I believe was
weee50. But to be fair, he does seem to be less active nowadays.
On the fourth hand, I only realized the cause of the duplicate bug after
stepping through it in GDB, instead of just looking at it and going "hey
wait a minute" earlier. I'm surprised it didn't take me longer to
realize the problem.
I'm not sure what all these hands mean anymore, or where I'm getting
extra hands from. Whatever. This regression is fixed now.
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.