Did another complete proofread of all the non-roomnames (hadn't looked
through *everything* in a while), and it's just three little things
that I felt would be important enough to tweak.
The strings "Vitellary"/"Vermilion"/"Verdigris"/"Victoria" now have two
cases to support changing them for the intermission replay menu options
(like "with Vitellary").
Also, the string "< and > keys change tool" is now "{button1} and
{button2} keys change tool", so it can be changed dynamically without
having to retranslate the string.
I wanted to not complicate the system with different string cases (like
cgettext) if possible, and I have been able to keep the main strings a
simple English=Translation mapping thus far, but apparently strings
like "Rescued!" (which are one string in English), have to be
translated for the correct gender in some languages. So this was a good
time to add support for string cases anyway.
It's a number that can be given to a string to specify the specific
case it's used, to disambiguate identical English keys. In the case of
"Rescued!" and "Missing...", male versions of the string are case 1,
female versions are case 2, and Viridian being missing is case 3. Of
course, if a language doesn't need to use different variants, it can
simply fill in the same string for the different cases.
If any other string needs to switch to different cases: distinguish
them in the English strings.xml with the case="N" attribute (N=1 and
higher), sync language files from the translator menu (existing
translations for the uncased string will simply be copied to all cases)
and change loc::gettext("...") to loc::gettext_case("...", 1),
loc::gettext_case("...", 2), etc.
With a single README.txt for both translators and maintainers, both
have to read through info that's not relevant for them, because
translators don't need to worry about the specifics of adding new
English strings and recompiling the game, and programmers don't need to
worry about the specifics of how to translate things. Now it's split
into README-translators.txt and README-programmers.txt.
I would, of course, recommend translators to translate the roomnames
while playing the full game (optionally in invincibility) so they can
immediately get all the context and maybe the most inspiration. And if
you want to go back into a specific level, then there's always the time
trials and intermission replays which will give you full coverage of
all the room names.
However, the time trials weren't really made for room name translation.
They have some annoying features like the instant restart when you
press ENTER at the wrong time, they remove context clues like
teleporters and companions, but the worst problem is that the last room
in a level is often completely untranslatable inside the time trials
because you immediately get sent to the results screen...
So, I added a new menu in the translator options, "explore game", which
gives you access to all the time trials and the two intermissions, from
the same menu. All these time trials (which they're still based off of,
under the hood) are stripped of the annoying features that come with
time trials. These are the changes I made to time trial behavior in
translator exploring mode:
- No 3-2-1-Go! countdown
- No on-screen time/death/shiny/par
- ENTER doesn't restart, and the map menu works. The entire map is also
revealed.
- Prize for the Reckless is in its normal form
- The teleporters in Entanglement Generator, Wheeler's Wormhole and
Level Complete are restored as context for room names (actually, we
should probably restore them in time trials anyway? Their "press to
teleport" prompt is already blocked out in time trials and they do
nothing other than being a checkpoint. I guess the reason they were
removed was to stop people from opening the teleporter menu when that
was not specifically blocked out in time trials yet.)
- The companions are there at the end of levels, and behave like in no
death mode (become happy and follow you to the teleporter). Also for
context.
- At the end of each level, you're not suddenly sent to the menu, but
you can use the teleporter at your leisure just like in the
intermission replays. In the Final Level, you do get sent to the menu
automatically, but after a longer delay.
I made another mark on VVVVVV: don't be startled, I added gamestates.
I wanted all teleporters at the end of levels to behave like the ones
at the end of the intermission replays, and all handling for
teleporting with specific companions is already done in gamestates, so
rather than adding conditional blocks across 5 or so different
gamestates, it made more sense to make a single gamestate for
"teleporting in translator exploring mode" (3090). I also added an
alternative to having to use gamestate 3500 or 82 for the end of the
final level: 3091-3092.
One other thing I want to add to the "explore game" menu: a per-level
count of how many room names are left to translate. That shouldn't be
too difficult, and I'm planning that for the next commit.
This is a single block of code so it was easy to split from the
foundation commit.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This involves loc::gettext_roomname and loc::gettext_roomname_special.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This mainly adds loc::gettext calls and replaces SDL_snprintf by
VFormat for the percentage.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This replaces SDL_snprintf by VFormat for the time strings, and makes
number_words get translated numbers.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This mainly adds loc::gettext calls for all the menu titles and
explanations. It also redesigns the time trial screen.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This mainly adds loc::gettext calls for menu option labels.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
setLevelDirError was changed from snprintf-style to VFormat, but it's
only used in that file so...
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This adds loc::gettext for all "Press {button} to explode" and friends,
and also changes the interact_prompt function in Render.cpp to expect
{button} instead of %s.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
The affected functions are:
- editormenuactionpress
- editorinput
- editorclass::switch_tileset
- editorclass::switch_tilecol
- editorclass::switch_enemy
- editorclass::switch_warpdir
This mainly adds loc::gettext calls.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This commit adds most of the code changes necessary for making the game
translatable, but does not yet "unhardcode" nearly all of the strings
(except in a few cases where it was hard to separate added
loc::gettexts from foundational code changes, or all the localization-
related menus which were also added by this commit.)
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This is needed for the limits check in the translator menu.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This just adds booleans roomname_special to the level classes in
preparation for the localization system to use them.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
A relevant paragraph copied from the original commit history:
The idea is that we store all strings somewhere managed, and then the
hashmap only needs pointers to those strings. For storing strings, I
created a `Textbook` structure, which consists of one or more 50 KB
"pages" (allocated as needed) on which you can simply write strings in
both languages back-to-back with `textbook_store(textbook, text)` and
get pointers to each of them. (I was originally going to just use one
big buffer and realloc to double the size when filled up, but then the
hashmap would be full of dangling pointers...) When needed, like when
switching to a different language, an entire textbook can be freed at
once.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
This has a lot of characters that different languages need.
This commit is part of rewritten history of the localization branch.
The original (unsquashed) commit history can be found here:
https://github.com/Dav999-v/VVVVVV/tree/localization-orig
I think this is because if you both check that __has_builtin is defined
and use it in the same 'if' preprocessor statement, it can error because
there's no equivalent to short-circuiting in preprocessor statements.
_SDL_HAS_BUILTIN should be safer.
This creates the game over screen for dying in No Death Mode. It's three
lines long and it's only called once. There's no reason it has to be a
separate function. From the name it sounds like it was meant to be a
generic function but it's anything but that. So just inline it in to
where it's called.
This fixes a bug where players could flip in mid-air at the start of
custom levels whose start points were in mid-air, because
onground/onroof were not being reset. This could also be done when
loading in to a save with a checkpoint in mid-air. All that's required
is to exit the game while standing on a surface (or otherwise having a
nonzero onground/onroof).
This reminded me that there were other variables on the player entity
persisting through that made loading in to a level not have a totally
clean slate, such as walkingframe (which could affect sequencing
individual TASes together into one TAS), so it's better to just destroy
the player entity and recreate it.
Except some attributes still have to be persisted for 2.2 and 2.0
glitchrunner mode. But it's better that this is done via a whitelist
rather than a blacklist.
The player duplicate removal code in hardreset is mostly redundant now
(except for some very unlikely corner cases), but there's nothing wrong
with having redundant code as long as it's not harmful. I had a
paragraph here justifying why it could be removed but decided it was
simpler to just keep it.
This print is useful to know if an achievement (one that's not already
unlocked) would actually be unlocked in an end user environment, while
running the game in a dev environment.
Also fixed up the style of the function because it was definitely
inconsistent with the surrounding code.
This overhauls scriptclass::gamemode massively.
The first change is that it now uses an enum, and enforces using that
enum via using its type instead of an int. This is because whenever
you're reading any calls to startgamemode, you have no idea what magic
number actually corresponds to what unless you read startgamemode
itself. And when you do read it, not every case is commented adequately,
so you'd have to do more work to figure out what each case is. With the
enum, it's obvious and self-evident, and that also removes the need for
all the comments in the function too. Some math is still done on mode
variables (to simplify time trial code), but it's okay, we can just cast
between int and the enum as needed.
The second is that common code is now de-duplicated. There was a lot of
code that every case does, such as calling hardreset, setting Flip Mode,
resetting the player, calling gotoroom and so on.
Now some code may be duplicated between cases, so I've tried to group up
similar cases where possible (most notable example is grouping up the
main game and No Death Mode cases together). But some code still might
be duplicated in the end. Which is okay - I could've tried to
de-duplicate it further but that just results in logic relevant to a
specific case that's located far from the actual case itself. It's much
better to leave things like setting fademode or loading scripts in the
case itself.
This also fixes a bug since 2.3 where playing No Death Mode (and never
opening and closing the options menu) and beating it would also give you
the Flip Mode trophy, since turning on the flag to invalidate Flip Mode
in startgamemode only happened for the main game cases and in previous
versions the game relied upon this flag being set when using a
teleporter for some reason (which I removed in 2.3). Now instead of
specifying it per case, I just do a !map.custommode check instead so it
covers every single case at once.
While refactoring scriptclass::startgamemode, I noticed that these
variables weren't being reset. Fortunately, this doesn't seem to have
affected anything because they get overwritten one way or another in
startgamemode. But it's good just to be defensive and reset them anyway.
They are not reset in 2.2 or 2.0 glitchrunner mode because dying during
exiting to the menu is needed for credits warp, and that means the
checkpoint position needs to be maintained through.
This is to indicate when a code path is absolutely, for certain, 100%
unreachable. Useful as the default case inside a case-switch that is for
sure 100% exhaustive because it's inside the case of another case-switch
(and the default case is there to suppress compiler warnings about the
case-switch not being exhaustive), which is a situation coming up in my
scriptclass::startgamemode refactor.
It does this by deliberately invoking undefined behavior, either using a
compiler builtin that does the same thing or being a noreturn function
that returns. (And undefined behavior is not undefined behavior if it is
not executed in a code path, otherwise all NULL checks would be useless
because it'd dereference something that could be NULL in another code
path.)
I have no idea why they were floats in the first place. They are
coordinates, and coordinates are integer positions in this game. They
get converted to float only to be explicitly `static_cast`ed back to
ints in `testwallsy`.
This variable passes along the rule of the entity, which is an int. No
idea why it was converted to a float. Thankfully this is only used for
an unused block type, so it doesn't really matter.
This makes it so room names are no longer pointers to someone else's
memory, and instead to set them you use `mapclass::setroomname`. If the
string is short enough to fit in a static, no-alloc buffer, then it gets
copied there. Otherwise, a new heap allocation is made that duplicates
the string, and the new pointer is used instead.
This makes it possible for room names to contain arbitrary data whose
origin is temporary (e.g. from a script command that could be added in
the future).
This replaces all calls to SDL_free with a new macro, VVV_free, that
nulls the pointer afterwards. This mitigates any use-after-frees and
also completely eliminates double-frees. The same is done for any
function to free specific objects such as SDL_FreeSurface, with the
VVV_freefunc macro.
No exceptions for any of these calls, even if the pointer is discarded
or zeroed afterwards anyway. Better safe than sorry.
This is a macro rather than a function that takes in a
pointer-to-pointer because such a function would have type issues that
require casting and that's just not safe.
Even though SDL_free and other SDL functions already check for NULL, the
macro has a NULL check for other functions that don't. For example,
FAudioVoice_DestroyVoice does not check for NULL.
FILESYSTEM_freeMemory has been axed in favor of VVV_free because it
functionally does the same thing except for `unsigned char*` only.
Trinket and teleporter legends would be drawn even if they were out of
bounds. Trinket legends in particular were easy to do because you can
just place a trinket in a custom level and resize the map to not include
the room of the trinket.
Now, there are checks added so they won't be added if they are out of
bounds. This is in line with the fact that, since 2.3, if a trinket
exists outside of the map in custom levels, it won't count towards the
number of trinkets.
It's becoming pretty clear that the size of the map is important enough
to be queried a lot, but each time it's something like `map.custommode ?
map.customwidth : 20` and `map.custommode ? map.customheight : 20` which
is not ideal because of copy-pasting.
Furthermore, even `map.customwidth` and `map.customheight` are just
duplicates of `cl.mapwidth` and `cl.mapheight`, which are only set in
`customlevelclass::generatecustomminimap`. This is a bit annoying if you
want to, say, add checks that depend on the width and height of the
custom map in `mapclass::initcustommapdata`, but `map.customwidth` and
`map.customheight` are out of date because `generatecustomminimap`
hasn't been called yet. And doing the ternary there requires a `#ifndef
NO_CUSTOM_LEVELS` to reference `cl.mapwidth` and `cl.mapheight` which is
just awful.
So I'm axing `map.customwidth` and `map.customheight`, and I'm axing all
the ternaries that are duplicating the source of truth in
`MapRenderData`. Instead, there will just be one function to call for
the width and height, `mapclass::getwidth` and `mapclass::getheight`,
and everyone can simply call those without needing to do ternaries or
duplication.
The existing code was allergic to putting spaces between tokens, and had
some minor code duplication that I took the time to clean up as well.
The logic should be easier to follow now that the for-loops are no
longer duplicated for each of the map zoom levels.
I tested this by temporarily disabling map fog entirely and going
through a couple different custom levels to compare their minimap with
the existing code. These were A New Dimension and 333333 for 1x, Golden
Spiral and VVVV 4k for 2x, and VVVVVV is NP-Hard for 4x. There was no
difference in the output, not even a single pixel.
This adds color support to the output of the console on Windows. Now if
you're using Windows 10 build 1511 or later (I think it's build 1511
anyway; they added more VT sequence support in later versions), you will
see colors by default. This isn't due to Windows helping in any way;
this commit has to specifically enable it with SetConsoleMode() because
by default, Windows won't enable color support unless we enable it. (Or
if it's enabled in the registry, but having to go through the registry
to enable basic shit like that is completely fucking stupid.)
I tested this in my Windows 10 virtual machine and it's completely
working.
Previously, we were using `color_enabled` to mean both that the color
was supported and that it was enabled by the user (which it is enabled
by default). But this logic doesn't work well if the color check
function is called again and ends up enabling color after the user
disabled it. To fix this, just separate the two so the user controls one
`color_supported` variable and the `color_enabled` variable is separate.
Check both of them in order to print color, of course.
This adds the `-console` command-line option (for Win32 only) so the
game can spawn an attached console window which will contain all console
output.
This is to make it easier for people to debug on Windows systems.
Otherwise, the only way to get console output would be to either compile
the application as a console app (i.e. switch the subsystem to console)
- which is undesirable for regular users as this makes it so a console
is always spawned even when unwanted - or launch the game with shell
arguments that make it so output is redirected to a file.
As a result, color checking support is factored out of vlog_init() into
its own function, even though we don't support colors on Windows.
Using SDL_GetTicks() to seed the Gravitron RNG caused many
reproducibility issues while syncing https://tasvideos.org/7575S . To
fix this, add a frame counter, which is a number that is incremented
every frame and never resets, and use it instead.
If someone needs to switch back to SDL_GetTicks() for old TASes, then
provide the -seed-use-sdl-getticks command-line option for them.
In its previous location, it would only print the value of `s` after it
had been mutated by `splitmix32` four times, and it doesn't get used
after that, so the print isn't very useful.
Mixing code and declarations here is fine because starting from a few
months ago, we compile with C99 and if we ever need to compile with C90
then it's trivial to add braces surrounding the declarations.
If a music track has a loop comment with a negative value, ignore all
comments of the track. This is just to prevent any weirdness from
happening as it's safer to just let the track loop improperly. Also log
to the console to let users know.
This is the same thing that SDL_mixer does now:
libsdl-org/SDL_mixer@e819489459
This commit happened as a result of discussion on the VVVVVV Discord
server about SDL_mixer 2.0.4 behavior with weird loop comment values
(e.g. octal input with leading zeroes). This is simply updating the code
to be in line with what newer versions of SDL_mixer do.
Just in case it happens. Comments aren't really important to the game
(at worst a track will just loop in the wrong place) so it's fine to
carry on here and ignore all comments if this happens.
This does the following:
- Const-qualify variables if they are not modified
- Place each statement onto their own separate lines
- Place the asterisk with the type instead of the variable name
- Combine declarations and initializations where possible
VVVVVV uses submodules now, so you need to know how to initialize them.
I'm explicitly not including `git clone --recurse-submodules`. Usage of
submodules in git projects is kinda rare in my experience, so people
are used to doing simple clones, and that instruction would just result
in people being annoyed thinking they have to delete the repo they
already cloned, and clone it again except slightly differently.
It also doesn't help you if you need submodules that aren't in the
master branch (for example, if you clone my fork recursively and then
checkout the localization branch, you won't have C-HashMap and you'll
need the update command anyway). And you also need it whenever VVVVVV
updates its submodules. So teaching people just the update command is
better.
They weren't ever being used, and nobody really ever uses the return
value from the printf family of functions anyway. They return how many
bytes were actually printed, but if it's less than you expected then
there's not much you can really do about them. Also the vlog_* functions
were computing them inaccurately because I only set the return value to
the return value of vprintf when there's other print functions being
called, but regardless there's no reason to have a return value here
anyway.
The hilariously-named WIN32_LEAN_AND_MEAN slims down the number of
header files included in the already-massive `windows.h`. I know people
say Moore's law and precompiled headers and all that (well, we don't use
precompiled headers), but they kinda forgot about virtual machines, and
there's no reason not to define this and slim down the number of headers
anyway.
This started when I saw the warning that GetVersionExW was deprecated,
then looked it up and found StackOverflow answers saying that you should
basically detect the feature directly instead of checking the version,
which makes sense to me. Then I found that I could probably detect color
support by using GetConsoleMode and GetStdHandle. But then I asked
myself what the point was unless you could get color output directly in
the terminal, which it seems like you really can't if your app is a GUI
app. (I have no idea why Windows makes this pointless distinction
between console and GUI apps...)
I tested Command Prompt, PowerShell, Windows Terminal (which is just
PowerShell again), and even Git Bash (MINGW64), but none of them will
ever give the console output of a GUI app such as VVVVVV. The closest I
got is that Git Bash doesn't seem to detach the process, but it will
simply produce no output.
At this point I feel like it's not worth it keeping this code around if
it didn't even work in the first place, so I'm removing it. People can
always enable color by using the -forcecolor command-line argument
anyway.
I'm fine with putting the release version in a header file, thus
necessitating the need to recompile every file that includes it if it's
changed, simply because it's not supposed to be changed that often.
The SDL_arraysize is necessary because sometimes we'll have subreleases
(e.g. 2.4.1, 2.4.2, 2.4.3), and who knows, maybe we'll get to 2.10
someday.
This reworks how the commit hash and date are compiled so that if
they're changed (and they're changed often), only one source file needs
to be recompiled in order to update it everywhere in the game, no matter
how many source files use the hash or date.
The commit hash and date are now declared in InterimVersion.h (and they
need `extern "C"` guards because otherwise it results in a link fail on
MSVC because MSVC is stupid).
To do this, what now happens is that upon every rebuild,
InterimVersion.in.c is processed to create InterimVersion.out.c, then
InterimVersion.out.c is compiled into its own static library that is
then linked with VVVVVV.
(Why didn't I just simply add it to the list of VVVVVV source files?
Well, doing it _now_ does nothing because at that point the horse is
already out of the barn, and the VVVVVV executable has already been
declared, so I can't modify its sources. And I can't do it before
either, because we depend on the VVVVVV executable existing to do the
interim version logic. I could probably work around this by cleverly
moving around lines, but that'd separate related logic from each other.)
And yes, the naming convention has changed. Not only did I rename
Version to InterimVersion (to clearly differentiate it from
ReleaseVersion, which I'll be adding later), I also named the files
InterimVersion.in.c and InterimVersion.out.c instead of
InterimVersion.c.in and InterimVersion.c.out. I needed to put the file
extension on the end because otherwise CMake wouldn't recognize what
kind of language it is, and I mean like yeah duh of course it doesn't,
my text editor doesn't recognize it either.
I thought all of these were removed earlier but apparently not. Anyways,
add_definitions is bad because it pollutes the definitions of every
single target, we should be using target_compile_definitions instead.
This updates all references to SDL 2.0.22 to SDL 2.24.0, including the
Docker container that I maintain for Linux CI.
Be warned, this release of SDL updates the versioning scheme to be less
dumb. The previous version is 2.0.22, this release is 2.24.0 (so the
last number can be properly used for patch-level version releases).
This option is enabled by default and will replace absolute paths of all
source directory file paths with relative paths in the compiled binary,
if the compiler supports it. Of course, this isn't needed if you compile
with all paths removed anyways (e.g. in Release mode).
The purpose is to help make builds reproducible and to remove any
potentially sensitive information about the user or the user's system
from the compiled binary.
Both Clang and GCC support -fdebug-prefix-map, -fmacro-prefix-map, and
-ffile-prefix-map. In particular, -ffile-prefix-map is just a flag that
does both -fdebug-prefix-map and -fmacro-prefix-map.
According to https://reproducible-builds.org/docs/build-path/ ,
-fdebug-prefix-map is available in all GCC versions but only available
starting from Clang 3.8, and -fmacro-prefix-map and -ffile-prefix-map
are available since GCC 8 and Clang 10. So we check the compiler version
and use the available flags depending on if the compiler supports it or
not.
This does make debugging a bit more annoying, but there are a couple
ways to rectify this. Either disable it with
-DREMOVE_ABSOLUTE_PATHS=OFF, or add a `.gdbinit` that consists of
set substitute-path . ../..
so that `.` is considered to be `../..`. Of course, if you need to,
replace `../..` with the actual source directory path (in my case it's
`../../..` because I place my build folders in another subdirectory to
have multiple build folders in one directory).
This doesn't need to be a global `.gdbinit`, it can be in a
directory-specific `.gdbinit` (similar to how `.gitignore`s can also be
directory-specific). But then you need to add `add-auto-load-safe-path`
to your `.gdbinit` to load any directory-specific `.gdbinit`s.
The above is for GDB; I don't know what (if anything) needs to be done
for LLDB; I don't use LLDB.
Fixes#889.
Whereas all `SDL_assert`s will go away when compiling with optimization
flags and all plain `assert` calls (used in PhysFS) will go away when
compiling in Release mode, FAudio has a bunch of debug stuff that needs
to be explicitly disabled with its own `FAUDIO`-prefixed flag.
To do this in Release mode, we need to use generator expressions for
dumb CMake reasons. Basically, if checking the CMAKE_BUILD_TYPE variable
will not work for certain generators (Ninja, Visual Studio) because they
only specify the build type at build time, not generation/configuration
time.
This is so flags that apply globally (i.e. to the game and all static
libraries it's compiled with), such as /MT on MSVC, can be put in a
list, and along with putting all static libraries in a list, we remove
the need for each flag to be repeated for each static library and we can
just use a foreach loop instead.
(Global compile flags of course don't apply to us meddling with
CMAKE_C_FLAGS and CMAKE_CXX_FLAGS directly, because we need to do that
in order to make sure the C and C++ standards are set properly.)
This fixes a regression where the game ignored the amount of frames you
held down a direction if you released the direction during death.
Previously, the game only checked the amount of frames you held down a
direction if you were able to control the player. If you weren't able to
control the player (e.g. during the death animation), then the number of
frames it counted didn't change. This also meant that if you were
holding a direction before you died, but released it during death, the
game wouldn't zero out the number of frames you held it.
This behavior was useful because it meant you could keep the
deceleration momentum that you normally get by holding a direction for 5
frames just by holding a direction for less than 5 frames after dying,
if you had the rest of the hold frames before you died. This behavior is
what's used in https://tasvideos.org/7575S at around frame 7200.
Unfortunately, #609 made it so that the direction hold processing
happened even if the player didn't have control, meaning that it would
zero the hold frames during the death animation in the TAS, thus
desyncing it when it performed the maneuver it relied on the extra
momentum for after Viridian respawns.
The solution here is to just add the check back in again.
Fixes#887.
While fixing #885, I noticed that I had a bunch of
`special/stdin.vvvvvv` entries saved in my `levelstats.vvv`. At once I
knew that the dumb `special/stdin` hack that actually checks if the
filename passed is `special/stdin` was to blame.
STDIN playtesting was first merged, I knew in the back of my mind that
it was a bit of a dumb hack, but I didn't know it would cause
consequences like showing up in `levelstats.vvv`. For now, I'll just
have to patch it, but hopefully in the future I'll remove the dumb hack
entirely. Commenting both instances of the dumb hack with instructions
to grep for it should help maintainers out.
This is useful to investigate any TAS desync/reproducibility issues
relating to RNG, because even though I specifically separated the
Gravitron RNG away from other RNG and made it not dependent on the
system libc rand() function, there's still apparently some differences
in RNG execution between systems, resulting in TASVideos submission 7575
( https://tasvideos.org/7575S ) not syncing for everyone except the
author.
It seems that SDL_GetTicks(), which is used to seed the xoshiro RNG, is
not reliably consistent between systems, so in the future I will
probably replace it with a counter that is incremented each frame
starting from game startup, which is probably better.
This fixes the following warnings:
desktop_version/src/Music.cpp: At global scope:
desktop_version/src/Music.cpp:240:23: warning: non-static data member initializers only available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]
240 | Uint8 *wav_buffer = NULL;
| ^
desktop_version/src/Music.cpp:414:32: warning: non-static data member initializers only available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]
414 | Uint8* decoded_buf_playing = NULL;
| ^
desktop_version/src/Music.cpp:415:32: warning: non-static data member initializers only available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]
415 | Uint8* decoded_buf_reserve = NULL;
| ^
desktop_version/src/Music.cpp:416:21: warning: non-static data member initializers only available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]
416 | Uint8* read_buf = NULL;
| ^
These warnings are because the non-static data members (i.e. data
members that will be different for each instance of a class) are being
initialized at the same time as they're being declared, which means
that's what their value will be when the class instance is initialized.
However, this is only a C++11 feature, and we don't use C++11. Luckily,
we don't need to do this, and this is in fact redundant because we
already zero out the class instance in its constructor using
SDL_zerop(). Therefore, we should remove these initializers to fix
compliance with C++03.
Instead, for any number that isn't in the list of number words, just
return the regular Arabic numerical representation (i.e. just convert it
to string). It's better than having "Lots" or "???", neither of which
really tell you what the number actually is.
This flag makes it so the MSVC runtime libraries are statically linked.
This avoids needing Windows users to have these libraries installed.
Apparently /MT stands for "MultiThreaded", and there's a bit of a
history there where originally by default you could only have a
single-threaded library, and then the multi-threaded flags were added in
later.
First I tried doing target_compile_options on VVVVVV, but then got a
linker error. Then I tried doing add_compile_options because I figured
/MT had to be applied everywhere, and it seemed to work, but it still
linked to the runtime libraries. Apparently it was being overridden.
Then I tried target_compile_options again but this time did it to
everything, and that linked correctly and also removed the runtime
dependency. I would've tried using the MSVC_RUNTIME_LIBRARY property
- along with the CMP0091 policy - but those were only introduced in
CMake 3.15.
You can verify that a binary is built without dependencies by installing
LLVM and running llvm-readobj --needed-libs path/to/binary. This is the
output for a binary with runtime dependencies:
infoteddy@fedorarune ~/d llvm-readobj --needed-libs VVVVVV.exe
File: VVVVVV.exe
Format: COFF-i386
Arch: i386
AddressSize: 32bit
NeededLibraries [
ADVAPI32.dll
KERNEL32.dll
MSVCP140.dll
SDL2.dll
SHELL32.dll
USER32.dll
VCRUNTIME140.dll
api-ms-win-crt-heap-l1-1-0.dll
api-ms-win-crt-locale-l1-1-0.dll
api-ms-win-crt-math-l1-1-0.dll
api-ms-win-crt-runtime-l1-1-0.dll
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-string-l1-1-0.dll
api-ms-win-crt-time-l1-1-0.dll
api-ms-win-crt-utility-l1-1-0.dll
]
And this is the output for a binary with those dependencies having been
statically-linked in:
infoteddy@fedorarune ~/d llvm-readobj --needed-libs VVVVVV.exe
File: VVVVVV.exe
Format: COFF-i386
Arch: i386
AddressSize: 32bit
NeededLibraries [
ADVAPI32.dll
KERNEL32.dll
SDL2.dll
SHELL32.dll
USER32.dll
]
As already described in cc61194bed, as
well as Ved's commits from the last almost two weeks, starting VVVVVV
from Ved for playtesting could be made a lot faster by "preloading" the
game - letting it do all its asset loading in the background without
creating a window - and then waiting until the level is passed in via
stdin. There's only one problem left with this approach: VVVVVV
currently expects the starting position to be passed via command line
arguments, which isn't known yet at the time we'd like to start VVVVVV.
Therefore, this commit allows passing the starting position via the
level XML, instead of via arguments.
The extra XML looks like this, and is added next to the <Data> tag:
<Playtest>
<playx>214</playx>
<playy>112</playy>
<playrx>100</playrx>
<playry>100</playry>
<playgc>0</playgc>
<playmusic>4</playmusic>
</Playtest>
This is handled similarly to how the equivalent arguments are handled:
when the level metadata is loaded for CLI playtesting, we also try to
find this tag, and if it exists, it sets the same variables that the
arguments would have otherwise set.
There's always been a bit of an inconsistency in the game where enabling
invincibility would make spikes so solid that enemies and moving
platforms would treat spikes as solid and bounces off of them.
This fixes that by adding an `invincible` parameter to collision
functions, so the functions will only treat spikes as solid if that
parameter is true, and the parameter passed will only be true if it's
called for an entity that is a humanoid and invincibility mode is
enabled.
Also, for clarity, `spikecollide` is renamed to `towerspikecollide`
because it's only used for tower spikes. And as a small optimization,
`checktowerspikes` returns early if invincibility mode is enabled.
This is a minor optimization to streamline the experience of Ved
playtesting. Previously, the user would have to wait for all the assets
to load when launching playtesting (most of the time, I suspect, is
taken up by loading music from a vvvvvvmusic blob). With this
optimization, however, the game can be launched in the background and
its assets can be loaded, while it blocks on STDIN input. During this
time, the user in Ved will be choosing where to start playtesting. After
Ved provides STDIN input, then the window will be created and appears
instantaneously.
This also fixes a related issue in which providing an invalid
playtesting level name would result in a brief window flash that gets
instantly destroyed. With this, if the level is invalid then no window
is ever shown at all.
Probably should have done this earlier in 2.3, but better late than
never.
This makes it easier for third-party programs like Ved to detect what
version of the game this is.
Slightly quick-n-dirty for now, I'll de-duplicate the version number
later, and add commit hash and date if applicable.
Without this, entering in-game and opening the map with missing graphics
will result in a segfault. This is because even if the image doesn't
exist, it's still pushed into the `images` std::vector as a NULL
pointer. And it segfaults because we dereference it (to get things like
their width and height). In contrast, passing them to SDL is fine
because SDL always checks for NULL first.
There are three different places where we call PHYSFS_openRead. This
commit makes sure all of them print a statement upon failure along with
the PhysFS reason for failure, and assigns the log level of each print
as so:
- FILESYSTEM_loadFileToMemory: Debug print (previously no
print existed in the first place), because some files (such as
font.txt) may or may not be needed, but if it isn't then no need to
print and worry the user. The game will error anyway if a critical
file like a graphics file is missing.
- FILESYSTEM_loadBinaryBlob: Debug print (previously info print),
because sometimes it's not needed, such as mmmmmm.vvv. I remember one
user being worried that the game printed "Unable to open file
mmmmmm.vvv" when it's not critical unlike vvvvvvmusic.vvv (and that
file is assumed to exist if data.zip exists anyways). Though maybe we
should move to loose-leaf files to save on memory usage (and so we
don't have to use special tools to modify a binary blob)...
- FILESYSTEM_loadZip: Error print. If we're calling this function, we
really do expect the zip to be loaded, and if it fails because we
can't open the file in the first place, then it would be good to know
why.
This commit adds a new string formatting system to replace uses of
`SDL_snprintf` and string concatenation.
Making our own string formatting system has been briefly discussed
during the review of the localization branch, and on the VVVVVV
Discord. It's inspired by Python's format strings, but simpler.
This is primarily to benefit localization - strings will be easier to
understand (`Now using %s Tileset` → `Now using {area} Tileset`,
`"%s remain"` → `"{n_crewmates|wordy} remain"`), translators can change
the word order for their language's grammar (`%1$s` is a POSIX
extension), and this system is also less error-prone (making the format
string not align with the actual arguments won't result in a crash or
UB).
It also integrates our needs better - particularly the "wordy" numbers
without having to have a `help.number_words(n).c_str()` at the
callsite, translators can opt in and out of wordy numbers per string,
and this should also make it easier to solve #859.
This commit adds the formatting system itself, and changes one
`SDL_snprintf` in the code to use it as a small demo (the rest should
probably be done in the localization branch to avoid more unneeded
work).
The system is described in full detail in VFormat.h and in the pull
request description.
2.0.22 just released 40 minutes ago.
This also updates the `Dockerfile` to use the URL from the GitHub
releases page, instead of SDL's servers.
I've also pushed a new Docker container to
`ghcr.io/infoteddy/vvvvvv-build`.
This removes the magic numbers previously used for controlling the fade
mode, which are really not readable at all unless you already know what
they mean.
0: FADE_NONE
1: FADE_FULLY_BLACK
2: FADE_START_FADEOUT
3: FADE_FADING_OUT
4: FADE_START_FADEIN
5: FADE_FADING_IN
There is also the macro FADEMODE_IS_FADING, which indicates when the
intention is to only check if the game is fading right now, which wasn't
clearly conveyed previously.
I also took the opportunity to clean up the style of any lines I
touched. This included rewriting if-else chains into case-switches,
turning one-liner if-then statements into proper blocks, fixing up
comments, and even commenting the `fademode == FADE_NONE` on the tower
spike checks (which, it was previously undocumented why that check was
there, but I think I know why it's there).
As for type safety, we already get some by transforming the variable
types into the enum. Assignment is prohibited without a cast. But,
apparently, comparison is perfectly legal and won't even give so much as
a warning. To work around this and make absolutely sure I made all
existing comparisons now use the enum, I temporarily changed it to be an
`enum class`, which is a C++11 feature that makes it so all comparisons
are illegal. Unfortunately, it scopes them in a namespace with the same
name as a class, so I had to temporarily define macros to make sure my
existing code worked. I also had to temporarily up the standard in
CMakeLists.txt to get it to compile. But after all that was done, I
found the rest of the places where a comparison to an integer was used,
and fixed them.
Previously, it was copy-pasted and slightly different, when really, they
ought to both be the exact same code.
It kind of pains me that the room name, glitch name, and hidden name
don't own their own memory, but, that's to be addressed later.
What's a bit annoying is that the `temp` variable used in
`teleporterrender` also ends up being reused later in the function. In
this case, I opted to just redeclare them when they are used anyway, to
make it clearer.
Apart from `teleporterrender` no longer calling `map.area` or caring
about `map.custommode`, it also no longer cares about
`graphics.fademode` being 0. I could never actually get this condition
to be false in practice, and I have absolutely no idea why it's there.
I'm guessing it could be some weird edge case rendering issue if the
screen is fully black? But I wouldn't know how to trigger that, and
anyway it should probably be fixed elsewhere. So I'm just going to
remove that conditional.
It's quite rare, though possible, that during finalstretch you could see
a glitchy tileset that looked like this:
https://i.imgur.com/V7cYKDW.png
This happened because final_mapcol, the variable that controls which
color of finalstretch is rendered, could end up being 7. Normally, it's
in the range of 1..6, which perfectly correlates with the Warp Zone
tilesets in tiles2.png, and the higher the number the farther back in
the tileset it goes from the gray Warp Zone tileset. However, if it's 7,
then it'll start grabbing tiles from the Ship plus some unused blank
tiles, which does not look pretty in the slightest.
This happened because it's possible, though exceedingly unlikely, that
fRandom(), a function which returns a float between 0..1, could return
exactly 1. fRandom() calls rand(), which returns a result between 0 and
RAND_MAX, and divides it by RAND_MAX. This value is implementation
dependent, but required to be at least 32767, and on most systems is
2147483647. Even taking the value of 32767, that means there's a 0.003%
chance that you could get this glitchy tileset when the game cycled the
color in finalstretch. But of course, playing the game for long periods
of time will eventually increase this chance - cycling the color 1,000
times (around 17 minutes of playing) will result in the chance being 3%.
Then as the calculations in the finalstretch color cycling logic calls
fRandom(), then multiplies by 6 and adds 1, it was possible for
fRandom() to return exactly 1, then have
6 added to it, resulting in final_mapcol being 7.
To fix this, just decrement the multiplication by fRandom() to multiply
by 5 instead of 6. Now the only possible numbers that calculation can
produce would be 1..6.
This fixes a limitation where the level filename had to be the exact
same name as the name of the zip, because the game used the name of the
level to identify the zip of which to load assets, and this also made it
impossible to use assets for more than one level in a zip.
Instead, we just look up where the level came from, so we can always
load its assets regardless of its filename.
Additionally, the zip structure checks can go away too, simplifying the
code further.
This WOULD be a huge breaking change, if it weren't for the fact that no
one uses them. Which is why I'm removing them, to simplify the code.
I asked on the VVVVVV Discord whether anyone used them or was even aware
of them and basically the answer was no. I go on Distractionware and no
one uses them. And why would they, when they'd have to distribute the
level .vvvvvv file separately? Better to just distribute everything in
one zip. And it's quite a bit obscure that you have to suffix the file
with .data.zip anyway.
During review of #869, I looked at this part of the codebase again. I
have no idea how or why, but during the course of 2.4 this whole area
just became a mess.
The issues I fixed (in no particular order):
- Copy-pasting the code that loads from the binary blobs
- Making sure SDL_RWFromConstMem is used over SDL_RWFromMem wherever
possible
- Adding checks to make sure the index from the binary blob is valid
(it's possible it could not exist)
- Adding checks to make sure we gracefully handle
SDL_RWFromConstMem/PHYSFSRWOPS_openRead returning NULL
- Moving the pointer asterisk to the type instead of the name :)
So, it turns out we weren't quite done fighting CMake yet...
To accommodate #869 (and actually also #272), the C standard was raised
from C90 to C99. This turned out to require a bit of a fight with the
CentOS CI's CMake version to get it to set the flags we wanted (and to
not overwrite them later). Eventually the fix was to move the block
that sets the standards to later in the file, which was done in
24353a54bb.
As it apparently turns out, if your CMake is at least 3.1.3 and
`CMAKE_<LANG>_STANDARD` is used instead of the workaround, the standard
setting now has an effect on the third party libraries, but not on
VVVVVV itself. The cause is (probably) the phrase "if it is set when a
target is created" in the CMake documentation - the
`CMAKE_<LANG>_STANDARD` values have to come before the VVVVVV target is
defined. In other words, the compiler's default C/C++ standard will be
used, probably something like C17 and C++17. As I can confirm with
`__cplusplus` and `__STDC_VERSION__` with my recent-enough CMake. If I
force the pre-3.1.3 workaround to be used, everything is compiled with
C99/C++98 as expected; and the `-fno-exceptions` `-fno-rtti` flags
appear everywhere regardless of version.
So my fix is to make the CMakeLists a little less complex by
simplifying away the `CMAKE_<LANG>_STANDARD` and
`CMAKE_<LANG>_EXTENSIONS`, and always using the workaround regardless
of CMake version. There's nothing wrong with the workaround, the same
thing is also done for `-fno-exceptions` `-fno-rtti`, and it's good to
have a less complicated CMakeLists that doesn't do different and
unexpected things for different versions.
The previous commit f6d7a214f8 ended up
breaking CI because the workaround ended up breaking the PhysFS build
too, which was previously relying on extensions to compile.
Since #869 is going to require C99 anyways, I might as well just up the
standard now. That way the PR won't have to fight it too.
Previously, if the user had a CMake version below 3.1.3, we told them to
set `-std` themselves.
However, we are going to go to C99 soon (because of FAudio, see #869),
and CentOS 7's CMake is too old to set `-std=` automatically, defaulting
to C90. This is bad because it fails the CI.
To work around this, we set `-std=` ourselves, but first we have to
clear any existing `-std=` flag in C_FLAGS or CXX_FLAGS. Amusingly
enough, MSVC does not have `/std:` switches for either C90 or C++98, so
we just get to do nothing.
This isn't necessary, but it does silence these annoying logs if you
pass an invalid argument or don't have data.zip:
[ERROR] Could not get window size: Invalid renderer
[WARN] Stats not loaded! Not writing unlock.vvv.
[ERROR] Could not get window size: Invalid renderer
[WARN] Settings not loaded! Not writing settings.vvv.
To do this, I've added FILESYSTEM_isInit().
This means we are no longer copy-pasting PhysFS source files directly.
Since the source files reside in a src/ subdirectory, the paths in the
CMakeLists.txt have to be adjusted.
We are no longer copy-pasting LodePNG source files directly.
As we can't rename lodepng.cpp to lodepng.c in the submodule itself, we
need to make a wrapper file, lodepng_wrapper.c, that #includes
lodepng.cpp, but gets compiled as C.
This prevents writing to unlock.vvv or settings.vvv if the game hasn't
made an attempt to load them yet. Otherwise, if the game aborted via
VVV_exit() because of, say, failure to parse a graphics file, it would
overwrite perfectly existing valid save data since it hasn't loaded it
yet.
Fixes#870.
Another cause of #870 is d0ffafe117, as a
bisect tells me. What that commit did is remove screenbuffer as a
pointer, since it's a statically-allocated object that _should_ always
exist, and it removed the `screenbuffer == NULL` guards in savestats()
and savesettings(). Unfortunately, those guards did something very
important - namely, they prevented writing to the save files when the
filesystem wasn't initialized. But that wasn't made clear, because it
seemed like the point of those guards was to prevent dereferencing NULL.
So instead, explicitly make it clear that
FILESYSTEM_saveTiXml2Document() needs to fail if the filesystem isn't
initialized. I've done this by adding an isInit bool to
FileSystemUtils.cpp.
Issue #870 showed one of the problems that this game has, namely that it
only sometimes checks SDL return values, and did not do so in this case.
Part of the cause of #870 is that Screen::GetWindowSize does not check
the return value of SDL_GetRendererOutputSize, so when that function
fails (as in the case where m_renderer is NULL and does not exist), it
does not initialize the out values, so it ends up writing uninitialized
values to the save files.
We need to make sure every function's return value is checked, not just
SDL functions, but that will have to be done later.
While reviewing #272, I noticed that the PR was passing these two
arguments through a helper function, even though they really shouldn't
ever change. To obviate the need to pass these through, I'm making them
global variables.
pathSep is just a string literal from PhysFS, while basePath is a whole
complicated calculation from SDL and needs to be freed. It will be freed
upon filesystem deinit (as is done with PhysFS and the STDIN buffer).
Additionally the logic in FILESYSTEM_init is simplified by no longer
needing to keep a retval variable or use gotos to free basePath in
there.
This lets any script name use capitals and spaces all they want, while
still being able to jump to them via iftrinkets() or similar.
The issue is that whenever tokenize() is ran, all spaces are stripped
and every argument is lowercased before being put into `words`. So, the
solution here is to create a raw_words array that doesn't perform space
stripping or lowercasing, and to refer to that whenever there's a script
command that loads a script. We keep the lowercasing and space removal
elsewhere to be more forgiving to newcomers.
This is technically a forwards compatibility break, but it's only a
minor one, and all levels that utilize it can still be easily modified
to work on older versions anyway.
As reported by Dav999, Victoria and Vermilion's trophy colors are
swapped again in 2.4. He points to
37b7615b71, the commit where I fixed the
color masks of every single surface to always be RGB or RGBA.
It sounded plausible to me, because it did have to do with colors, after
all. However, it didn't make sense to me, because I was like, I didn't
touch the trophy colors at all after I originally fixed them.
After I ruled out the RGBf() function as a confounder, I decided to see
whether intentionally reversing the color order in RGBf() to be BGR
would do anything, and to my surprise it actually swapped the colors
back around and it didn't actually look bad.
And then I realized: Swapping the trophy colors between RGB and BGR
ordering results in similar colors that still look good, but are simply
wrong, but not so wrong that they take on a color that no crewmate uses,
so it'd appear as if the crewmates were swapped, when in reality the
only thing that was swapped was actually the color order of the colors.
Trying to fix this by swapping the colors again, I actively confused
colors 33 and 35 (Vermilion and Victoria) with colors 32 and 34
(Vitellary and Viridian), so I was confused when Vermilion and Victoria
weren't swapping. Then as a debugging step, I only changed 34 to 32
without swapping 32 as well, and then finally noticed that I was
swapping Vitellary and Viridian, because there were now two Vitellarys.
And then I was reminded that Vitellary and Viridian were also wrongly
swapped since 2.0 as well.
And so then I finally realized: The original comments accompanying the
colors were correct after all. The only problem was that they were fed
into a function, RGBf(), that read the colors backwards, because the
codebase habitually changed the color order on a whim and it was really
hard to reason out which color order should be used at a given time, so
it ended up reading RGB colors as BGR, while it looked like it was
passing them through as-is.
So what happened was that in the first place, RGBf() was swapping RGB to
BGR. Then I came and swapped Vermilion and Victoria, and Vitellary and
Viridian around. Then later I fixed all the color masks, so RGBf()
stopped swapping RGB and BGR around. But then this ended up swapping the
colors of Vermilion and Victoria, and Vitellary and Viridian once again!
Therefore, swapping Vermilion and Victoria, and Vitellary and Viridian
was incorrect. Or at least, not the fix to the root cause. The root
cause would be to swap the colors in RGBf(), but this would be sort of
confusing to reason about - at least if I didn't bother to just type the
RGB values into an image editor. But that doesn't fix the real issue,
which is that the game kept swapping RGB and BGR around in every corner
of the codebase.
I further confirmed that there was no more RGB or BGR swapping by
deleting the plus-one-divide-by-three transformation in RGBf() and
seeing if the colors looked okay. Now with the colors being brighter, I
could see that passing it straight through looked fine, but
intentionally reversing it to be BGR resulted in colors that at a
distance looked okay, but were either washed out or too bright. At least
finally I could use my 8 years of playing this game for something.
So in conclusion, actually, 37b7615b71
("Fix surface color masks") was the real fix, and
d271907f8c ("Fix Secret Lab Time Trial
trophies having wrong colors") was the real regression. It's just that
the regression came first, but it wasn't really a regression until I did
the other fix, so the fix isn't the regression, the regression is...
this is hurting my brain. Or the real regression was the friends we made
along the way, or something like that.
This is the most trivial bug ever caused by the technical debt of those
god-awful reversed color masks.
---
This reverts commit d271907f8c.
Fixes#862.
In hindsight, the FAudio pointer will likely be in SoundTrack since we will
want to keep the mastering voice closer to the sounds and their source voice
arrays, while the MusicTrack will likely just be one source voice that gets
PCM from different streams.
This looks redundant but will actually help in the transition to FAudio; we
mostly want to keep the game logic the same while reimplementing the current
mixer, weirdness and all. Once that's done and confirmed to be stable and
consistent we can start cutting out the workarounds and quirks.
This meant making the track vectors static, but that's kind of what we do with musicclass anyway?
In any case, this will make the transition to FAudio MUCH less invasive.
This is quite simple. Just use a function pointer that switches out
which function we're going to use.
...Or not. C++ syntax makes this a bit awful since the function is a
member of a class. Did I mention how much I don't like C++?
Issue #849 suggested making integer be the default on Big Picture and
Steam Deck, but after thinking about it more, I think it's better and
more simple to just default to integer mode in general.
Reason being that people in Big Picture shouldn't expect the picture to
look different if they're out of Big Picture but still in fullscreen, or
have the picture look different in fullscreen depending on if they
launched the game for the first time in Big Picture or not. And besides,
the less lines of code, the better. So I'm just making integer mode the
default.
This enum is to just make each mode be readable, instead of mysterious
0/1/2 values. It's not a strictly-typed enum because we still have to
serialize it as ints in the XML, but it's better than just leaving them
as ints.
This also adds a NUM_SCALING_MODES enum, so we don't have to hardcode
that 3 when cycling scaling modes anymore.
This is mainly to make sure the game is definitely set to fullscreen in
Big Picture and on the Steam Deck, and to also remove windowed options
that wouldn't make sense if you're not on a desktop (toggling
fullscreen, resize to nearest). Those options would also be removed on
console and mobile too.
There's a bit of an annoying bug where if you launch the game in forced
fullscreen mode, but then exit and relaunch in normal mode, your game
will have fullscreen window sizes but it won't be fullscreen. This is
because forced fullscreen mode tries to preserve your non-forced
fullscreen setting, but due to the way window sizes are stored and
queried, it can't preserve the non-forced window size. This is a bit
difficult to work around, so I'm just putting in a FIXME here because we
can fix it later and I'd rather have a slightly buggy forced fullscreen
mode than not have one at all.
Closes#849.
Here's my notes on all the existing functions and what kind of time
formats they output:
- Game::giventimestring(int hrs, int min, int sec)
H:MM:SS
MM:SS
- Game::timestring()
// uses game.hours/minutes/seconds
H:MM:SS
MM:SS
- Game::partimestring()
// uses game.timetrialpar (seconds)
MM:SS
- Game::resulttimestring()
// uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s)
MM:SS.CC
- Game::timetstring(int t)
// t = seconds
MM:SS
- Game::timestringcenti(char* buffer, const size_t buffer_size)
// uses game.hours/minutes/seconds/frames
H:MM:SS.CC
MM:SS.CC
- UtilityClass::timestring(int t)
// t = frames, 30 frames = 1 second
S:CC
M:SS:CC
This is kind of a mess, and there's a lot of functions that do the same
thing except using different variables. For localization, I also want
translators to be able to localize all these time formats - many
languages use the decimal comma instead of the decimal point (12:34,56)
maybe some languages really prefer something like 1時02分11秒44瞬...
Which I don't know to be correct, but it's good to be prepared for it
and not restrict translators arbitrarily to only changing ":" and "."
when we can start making the system better in the first place.
I added a new function, UtilityClass::format_time. This is the place
where all time formats come together, given the number of seconds and
optionally frames. I have simplified the above-mentioned functions
somewhat, but I haven't given them a complete refactor or renaming -
I mainly made sure that they all use the same backend so I can make the
formats consistent and properly localizable.
(And before we start shoving more temporary char buffers everywhere
just to get rid of the std::string's, maybe we need to think of a
globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a
register of sorts, for when any line of text needs to be made or
processed, then printed, and then goes unused. Maybe help.textrow,
or something like that.)
As for this commit, the available time formats are now more consistent
and changed a little in some places. Leading zeroes for the first unit
are now no longer included, time trial results and the Super Gravitron
can now display hours when they went to 60 minutes before, and we now
always use .CC instead of :CC. These are the formats:
- H:MM:SS
- H:MM:SS.CC
- M:SS
- M:SS.CC
- S.CC (only used when always_minutes=false, for the Gravitrons)
Here's what changes to the current functions:
- Game::partimestring() is removed - it was used in two places, and
could be replaced by game.timetstring(game.timetrialpar)
- Game::giventimestring(h,m,s) and Game::timestring() are now wrappers
for the other functions
- The four remaining functions (Game::resulttimestring(),
Game::timetstring(t), Game::timestringcenti(buffer, buffer_size)
and UtilityClass::timestring(t)) are now wrappers for the "central
function", UtilityClass::format_time.
- UtilityClass::twodigits(int t) is now unused so it's also removed.
- I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
This de-duplicates the code, simplifying the codebase and reducing the
number of code paths that needs to be maintained. It also adds
robustness checks to LoadIcon that weren't there before (checking that
loading the file succeeded and that decoding the file also succeeded).
Now, you might think that loading the image with alpha will change
things in some way. But actually, I tested it, and I'm pretty sure it
doesn't. Since my window manager, i3, doesn't display icons, I've had to
resort to this hacky multi-liner
( https://unix.stackexchange.com/a/48866 ) to dump the icon to a PAM
file. I don't know what a PAM file is and all my various attempts to
convert it into something readable failed. But what I did instead was
just grab the icon of the game before this commit (on 2.3, just to be
extra sure), and `diff`ed it with the grabbed icon now, and they end up
being the exact same file. So there's literally no difference.
The only other consideration is that LoadImage needs to be exported,
since it's implemented in GraphicsResources.cpp. I just opted to
forward-declare it right before LoadIcon in Screen.cpp, since it's
really the only other time it's used. No need to create a new header
file for it or anything.
This is just to simplify the function. I really don't see any point in
taking away the alpha for some images, other than to disappoint people
who mod the game assets. It just complicates loading the image with no
real gain. To reduce maintenance costs, let's remove this alternate code
path.
Also it's a default argument and I don't like default arguments.
This argument... doesn't do anything.
First off, setting it to true explicitly enables blending on the
resulting surface, which is kind of the exact opposite of the variable
name and is misleading to say the least? And secondly, SDL surfaces have
blending enabled by default anyways, so it still doesn't even do
anything.
It's also a default argument, and I'm not one to shy away from removing
such default arguments.
This includes:
- Removing the constructor in favor of actually being able to see that
there's an actual function called being made initializing the struct
- Removing the use of a reference in Screen::init() in favor of using a
pointer
- Adding the struct qualifier everywhere (it's not much typing),
although technically you could typedef it in C, but I'd rather much
not typedef just to remove a tag qualifier
I know earlier I removed the gameScreen extern in favor of using
screenbuffer, but that was only to be consistent. After further
consideration, I have found that it's actually really stupid.
There's no reason to be accessing it through screenbuffer, and it's
probably an artifact of 2.0-2.2 passing stack-allocated otherwise-global
classes everywhere through function arguments. Also, it leads to stupid
bugs where screenbuffer could potentially be NULL, which has already
resulted in various annoying crashes in the past. Although those could
be fixed by simply initializing screenbuffer at the very top of main(),
but, why not just scrap the whole thing anyway?
So that's what I'm doing.
As a nice side effect, I've removed the transitive include of Screen.h
from Graphics.h. This could've been done already since it only includes
it for the pointer anyway, but it's still good to do it now.
In aa7b63fa5f, I didn't notice that the
result was implicitly being converted to int by the min/max from before.
I instead added it to the existing char, but that resulted in a char
overflow (it's unsigned, so thankfully not undefined behavior).
But of course the entire point of that commit is to make it explicitly
clear when you are converting between types, intentionally or otherwise,
in min/max comparisons. So despite causing a regression (which I have
now fixed), at least it did its job.
It's been long overdue that this variable be named properly. 2.2 added
integer scaling mode (thanks Ethan), 2.3 renamed it to scaling mode. Now
2.4 will properly call it what it is so people won't be confused by it.
The ScreenSettings struct member is renamed from stretch to scalingMode
along with the Screen class member being renamed, as well as the
toggleStretchMode function being renamed to toggleScalingMode as well.
Unfortunately, due to compatibility, we can't change the <stretch> XML
tag.
VVV_min/max are functions that only operate on ints, and SDL_min/max are
macros that operate on any type but double-evaluate everything.
I know I more-or-less said earlier that SDL_min/max were dumb but I've
changed my mind and think it's better to use them, taking care to make
sure you don't double-evaluate, rather than trying to generate your own
litany of functions with either your own hand-rolled generation macros,
C++ templates, C11 generics, or GCC extensions (that last one you'd
technically use in a macro but it doesn't really matter), all of which
have more downsides than just not double-evaluating.
And the upside of not double-evaluating is that you're disencouraged
from having really complicated single-line min/max expressions and
encouraged to precompute the values beforehand anyway so the final
min/max is more readable. And furthermore you'll notice when you
yourself end up doing double-evaluations anyway. I removed a couple
instances of Graphics::len() being double-evaluated in this commit (as
well as cleaned up some other min/max-using code). Although the only
downside to those double-evaluations was unnecessary computation,
rather than checking the wrong result or having multiple side effects,
thankfully, it's still good to minimize double-evaluations where
possible.
The reason why the wall stuck flipping behavior happened in the first
place was because the code went like this:
if (jumppressed)
{
if (onground && gravitycontrol == 0)
{
gravitycontrol = 1;
}
if (onroof && gravitycontrol == 1)
{
gravitycontrol = 0;
}
}
Basically, if you were both on ground and on a roof (i.e. stuck in a
wall), you would flip, but then due to code order and the fact that the
statement is not connected to the previous one, you would immediately
unflip afterwards. But if you were already flipped then the only path
that can be taken is to unflip you, since it's the statement that
appears last.
52fceb3f69 replaces the onground/onroof
conditionals with any_onground/any_onroof, so any player entity would
allow you to flip. But otherwise the code is the same. So is that the
problem?
No; tracing it through with GDB reveals that when you flip,
gravitycontrol is being set to 1, but never being set to 0. And it turns
out that's because any_onroof is not getting set. And that happens
because of another thing that 52fceb3f69
did - which was to set any_onground/any_onroof to true if indeed any
player entity was on ground or on a roof.
Unfortunately, the way Leo did it was to make the two statements
mutually exclusive - an 'if'-'else if' instead of two separate
statements. So a single entity could not mark both any_onground and
any_onroof as true (and the majority of the time, you will be a single
entity).
Thus, the solution is to just drop that 'else'.
Fixes#855.
I noticed when going frame-by-frame in Vertigo that sometimes the
wrapping enemies at the top sometimes just "popped" in frame. This is
because the sprite warp code only draws the warping sprite of sprites at
the bottom of the screen if they're below y=210. However, the warp point
starts at y=232, and warp sprites can be at most 32x32, which is exactly
the case with the Vertigo sprites, which are exactly 32x32. So the warp
code should start warping sprites if they're below y=200 (232 - 32)
instead.
Horizontal warping also has this problem; it warps at x=320 and
starts drawing warp sprites at x=300, even though it should start
drawing at x=288 (320 - 32). I've gone ahead and fixed that as well.
This is just in case the background gets changed by a custom level or
something to be something that would otherwise result in bad contrast.
Also if it needs to go outside the box for some reason. And I just like
the look of the outline.
Whew, look at all those copy-pasted print statements!
Doing this because of the in-game timer feature. The text would
otherwise clash harshly with the timer otherwise. Even with the outline
it still clashes, but at least there's an outline so it's not as harsh.
This adds centiseconds to the in-game timer, as well as the time trial
timer.
This is to aid speedrun moderators in determining when exactly a run was
completed, which they can't easily do if the timer only has a precision
up to a second.
The problem was that it also needed to check that game.swnmode was true,
in addition to game.swngame being 1, to actually check that the Super
Gravitron was being played.
Currently, all game-gamestate variables are just ints. This is not
particularly type-safe, in case the number of enums changes. To verify
that all current uses of the game-gamestate variables actually use the
enums, change them to be typed with the enum instead.
(As an aside, we should probably rename this so that it can't be
confused with Terry's state machine that has several different ways to
exploit to warp you to the credits, but that's something to do later.)
You'll note that getting in to the glitchy state of the game (the state
where you could play the game after it had hardreset() called on it)
required the player to quit to menu with ingame_titlemode set to true.
Well, quitting to menu calls hardreset(). So if hardreset() is called
when quitting, then you can no longer preserve ingame_titlemode that
way. This is a bit overkill, but I'm just taking precautions.
The game will now assert if the main menu is created while
ingame_titlemode is true, or if we attempt to load into a mode while
it's true. And if assertions are disabled then it just stops doing it
anyway.
I don't think there's any way to get a glitched ingame_titlemode again,
ever since I removed save data deletion taking you back to the main
menu. But I've had enough bugs with the fact that we more-or-less use
the same state for main menu options and in-game options, and that
glitched ingame_titlemode bug DID just happen, so I'm taking
precautions.
The next commit will add logic that more-or-less quits the whole block
if ingame_titlemode, and instead of adding another layer of indentation
I will just pull this into its own function so we can use a return
statement.
While I was testing deleting data while you were in-game, I noticed that
deleting data gave you all the "Win with less than X deaths" trophies,
even if you never got any of them before deleting data. Well, it turns
out that if you have the best game death count of 0, then you win every
trophy, and if you have the best game death count of -1 then that means
you haven't completed the game yet.
This reset was added in e3bfc79d4a, so at
least it's not in 2.3, but I only have myself to blame for making this
mistake. Whoops.
Going back to the main menu allowed for glitchiness to occur if you
deleted your save data while in in-game options. This meant you could
then load back in to the game, and then quit to the menu, then open the
options and then jump back in-game, exploring the state of the game
after hardreset() had been called on it. Which is: pretty glitchy.
For example, this meant having your room coordinates be 0,0 (which is
different from 100,100, which is the actual 0,0, thanks for the
100-indexing Terry), which caused some of the room transitions to be
disabled because room transitions were disabled if the
game.door_up/down/left/right variables were -2 or less, and they were
computed based on room coordinates, which meant some of them went
negative if you were 0,0 and not 100,100. At least this was the case
until I removed those variables for, at best, doing nothing, and at
worst, being actively harmful.
Anyways, so deleting your save data now just takes you back to the
previous menu, much like deleting custom level data does. I don't know
why deleting save data put you back on the main menu in the first place.
It's not like the options menu needed to be reloaded or anything. I
checked and this was the behavior in 2.0 as well, so it was probably
added for a dumb reason.
I considered prohibiting data deletion if you were ingame_titlemode, but
as of the moment it seems to be okay (if albeit weird, e.g. returning to
menu while in Secret Lab doesn't place your cursor on the "play"
button), and I can always add such a prohibition later if it was really
causing problems. Can't think of anything bad off of the top of my head,
though.
Btw thanks to Elomavi for discovering that you could do this glitch.
Okay, so, this is the elephant sprite, right?
https://i.imgur.com/dtS70zk.png
This is how it looks in the actual game, when you stitch all the rooms
together:
https://i.imgur.com/aztVnFT.png
Looks kind of messed-up, doesn't it?
Okay, so, in the bottom two rooms (11,9) and (12,9), the elephant is
placed at y-position -152. But in (11,8) and (12,8), it's placed at
y-position 96. This is despite the fact that -152 plus 240 is 88, not
96.
Similarly, in the left two rooms (11,8) and (11,9), the elephant is
placed at x-position 64, but in the right two rooms (12,8) and (12,9),
the elephant is placed at -264. This is despite the fact that 64 minus
320 is -256, not -264.
All of this stems from the calculations in Otherlevel.cpp using offsets
of -248 and -328 instead of -240 and -320.
So there's an 8-pixel offset that causes the elephant to be chopped off
when viewed with all the rooms stitched together. Simple enough to fix.
For the y-position fixes, I decremented the initial 8-pixel multiplier
as well, else the elephant would sink into the floor.
And this is what the elephant looks like now after stitching:
https://i.imgur.com/27ePLm1.png
Thanks to Tzann for pointing this out.
These warnings are kinda spammy, and they make sense in principle.
vlog_error takes a format string, so passing it an arbitrary string
(even error messages from libraries) isn't a good idea.
Dvoid from Discord just reported a crash when trying to load a
custom tiles2.png that was encoded weirdly.
The problem is that we don't check the return value from LodePNG, so
LodePNG gives us a null pointer, and then
SDL_CreateRGBSurfaceWithFormatFrom doesn't check this null pointer,
which then propagates until we crash in SDL_ConvertSurfaceFormat (or
rather, one of its sub-functions), and we would probably crash somewhere
else anyway if it continued.
After properly checking LodePNG's return value, along with printing the
error, it turns out that Dvoid's custom tiles2.png had an "invalid CRC".
I don't know what this means but it sounds worrying. `feh` can read the
file correctly but it also reports a "CRC error".
While we can't fix Canonical, we can at least work around them, and help
people on Ubuntu out by linking them to my comment listing the
currently-known workarounds.
SDL_GetTicks64() is a function that got added in SDL 2.0.18, which is
just an SDL_GetTicks() without a value that wraps every ~49 days,
instead wrapping after the sun explodes and kills us all. Oh sorry,
didn't mean to get existential.
For now, put this behind an SDL_VERSION_ATLEAST guard, which will be
removed when SDL 2.0.18 officially releases and we can update to it.
My latest rebase of #624 (refactoring/splitting editor.cpp) accidentally
overwrote #787 and essentially reverted it entirely. So, add it back in.
This is the same as #787 except it uses the new names, uses SDL_INLINE
to inline the function, and uses named constants.
GCC warns on casting `void*` to function pointers. This is because the C
standard makes a clear distinction between pointers to objects (`void*`)
and pointers to functions (function pointers), and does not specify
anything related to being able to cast object pointers to function
pointers.
The warning message is factually wrong, though - it states that it is
forbidden by ISO C, when in fact it is not, and is actually just
unspecified.
We can't get rid of the cast entirely, because we need the explicit cast
(the C standard _does_ mandate you need an explicit cast when converting
between object pointers and function pointers), and at the end of the
day, this is simply how `SDL_LoadFunction()` works (and more
importantly, how `dlsym()` works), so we can't get rid of it, and we
have no reason to anyways since it means we don't have a hard runtime
dependency on Steam (unlike some other games) and casting `void*` to
function pointers always behaves well on every single platform we ship
on that supports Steam.
Unfortunately, this warning seems to be a part of -Wpedantic, and
there's no way to disable this warning specifically without disabling
-Wpedantic. Luckily, I've found a workaround - just cast to `intptr_t`
before casting to the function pointer. Hopefully the compiler doesn't
get smarter in the future and this ends up breaking or anything...
* Add `setactivityposition(x,y)`, add new textbox color `transparent`
This commit adds a new internal command as a part of the visual activity zone changes I've been making.
This one allows the user to reposition the activity zone to anywhere on the screen.
In addition, this commit adds the textbox color `transparent`, which just sets r, g and b to 0.
rgb(0, 0, 0) normally creates the color black, however in VVVVVV textboxes, it makes the background
of them invisible, and makes the text the off-white color which the game uses elsewhere.
* add new variables to hardreset
* Fix unwanted text centering; offset position by 16, 4
It makes sense for `setactivityposition(0, 0)` to place the activity zone in the default position,
so the x has been offset by 16, and the y has been offset by 4.
Text was being automatically centered, meaning any activity zone which wasn't centered had misplaced text.
This has been fixed by calculating the center manually, and offsetting it by the passed value.
In previous versions, the game mistakenly checked the wrong color
channel of sprites, checking the red channel instead of the alpha
channel. I abuse this in some of my levels. Then I broke it when
refactoring masks so the game now no longer checks the red channel but
seems to check the blue channel instead. So re-fix this to the previous
bug, and preserve the previous bug with a comment explaining why.
This broke when I was refactoring things earlier, because we no longer
have a direct reference to the contents array, instead using a copied
int. But we have a settile() function anyway, so why not use it?
It is impossible to get on the quicksave screen in time trials, because
Enter is always bound to restarting time trials in a time trial, and
there's no way to open the map screen otherwise.
So, I've decided to add a fun little message in case someone somehow
manages to get to this screen in a time trial.
As is typical, the code was copy-pasted to account for Flip Mode, and
then copy-pasted again to account for custom levels, leading to four
instances of the same code.
I clean this up while also improving code style. This is where the new
FLIP macro and the fixed PrintWrap help a lot - otherwise the "Game
saved ok!" screen would look really wrong without the height
corrections.
It now looks more like the FLIP macro in Render.cpp: The y-position is
simply the height of the area the object is being flipped in, minus the
y-position itself, minus the height of the object. So:
flipped_yp = constant - yp - height
This is just a mathematical simplification of the existing statement,
which is:
flipped_yp = yp + 2 * (constant/2 - yp) - height
Using algebra, the 2 distributes into the parentheses, so
flipped_yp = yp + constant - 2 * yp - height
And the two `yp`s add together, so
flipped_yp = constant - yp - height
It's more readable this way.
Also I am using a named constant instead of a hardcoded one.
Otherwise, the text will be in the wrong position compared to normal
mode.
PrintWrap is not used in Flip Mode yet, but it will be used on the map
screen in an upcoming change of mine. The FLIP macro in Render.cpp can't
help us there, since it would need to know the height of the wrapped
text at compile time, when the height is only figured out at runtime
based off of the string (or, well, right _now_ the string _is_ known,
but we are going to merge localization for 2.4, and it's better to
future-proof...), and only PrintWrap itself can figure out the height of
the text. (Or, well, I suppose you could call it from outside the
function, but that's not very separation-of-concernsy style.)
Flipping objects in Flip Mode needs to account for the heights of those
objects (that's why flipme text boxes in Flip Mode in 2.2 were
positioned wrongly).
Also, turn it into a macro instead of an inline function.
This changes the positions of all existing de-duplicated map menu text
in Flip Mode, but it'll be more correct.
I misread SDL's code and thought that SDL's `begin_code.h` was internal
only to SDL. It turns out you get it when you include basically any
header, such as `SDL_stdinc.h`. So use it directly instead of copying it
for our own.
Between accounting for Flip Mode and custom levels, this code was
copy-pasted three times, leading to _four_ instances of one code!
Anyways, I've cleaned it up. The position of the text in Flip Mode is
going to differ by 4 pixels from how it was previously, but that really
shouldn't matter.
While dying in No Death Mode was fixed to no longer say "One trinkets"
in 2.3, if you win in No Death Mode with one trinket, the game would say
"One trinkets".
So to fix this, just slot a ternary in there. The code is already kind
of bad anyways and is going to be refactored/de-STLed in the future
regardless, so I'm not feeling too badly about shoving a ternary in
there like that.
This macro is to make it so it won't be error-prone to write the
semi-confusing `(a % b + b) % b` statement, and you can just use an easy
macro instead.
Currently, the only places a positive modulo is needed is when switching
tilesets, enemies, and warp directions in the editor, as well as when
getting a tile in the tower, since towers just repeat themselves
vertically. Towers used this weird while-loop to sort of emulate a
modulo, which isn't half-bad, but is unnecessary, and I don't think any
compiler would recognize it as a modulo. (And if it's not optimized to a
proper modulo... what happens if the number being moduloed is really,
really big?)
Believe it or not, there are still some remnants of the ActionScript
coding standards in the codebase! And one of them sometimes pops up
whenever an integer division happens.
As it so happens, it seems like division in ActionScript automatically
produces a decimal number. So to prevent that, the game sometimes
subtracts off the remainder of the number to be divided before
performing the division on it.
Thus, we get statements that look like
(a - (a % b)) / b
And probably more parentheses surrounding it too, since it would be
copy-pasted into yet another larger expression, because of course it
would.
`(a % b)` here is subtracting the remainder of `a` divided by `b`, using
the modulo operator, before it gets divided by `b`. Thus, the number
will always be divisible by `b`, so dividing it will mathematically not
produce a decimal number.
Needless to say, this is unnecessary, and very unreadable. In fact, when
I saw these for the first time, I thought they were overcomplicated
_modulos_, _not_ integer division! In C and C++, dividing an integer by
an integer will always result in an integer, so there's no need to do
all this runaround just to divide two integers.
To find all of these, I used the command
rg --pcre2 '(.+?).+?-.+?(?=\1).+?%.+?([\d]+?).+?\/.+?(?=\2)'
which basically matches expressions of the form 'a - a % b / b', where
'a' and 'b' are identical and there could be any characters in the
spaces.
There's really no need to put the y-multiplication in a lookup table.
The compiler will optimize the multiplication better than putting it in
a lookup table will.
To improve readability and to hardcode things less, the new
SCREEN_WIDTH_TILES and SCREEN_HEIGHT_TILES constant names are used, as
well as adding a new TILE_IDX macro to calculate the index of a tile in
a concatenated-rows (row-major in formal parlance) array. Also, tile
numbers are stored in a temporary variable to improve readability as
well (no more copy-pasting `contents[i + vmult[j]]` over and over
again).
There's really no reason for this simple multiplication plus division to
be in a lookup table. The compiler will optimize it faster than putting
it in a lookup table will, I'm sure.
This comment was referring to a now-deleted variable named mkdirResult
that was binary-"or"ed with all mkdir() results... except for the saves
directory. That variable was only used for save file migration, which is
now axed, so this comment is referring to nothing now.
I don't really know the answer to Ethan's question, but it doesn't
matter now.
So, it turns out freeing everything in binaryBlob::clear() without
checking for NULL results in an abort() because clear() gets called on
musicWriteBlob after it attempts to write the compiled music. It's just
that no one's using VVV_COMPILEMUSIC, so no one's ran into this.
I'm keeping VVV_COMPILEMUSIC around so in the future people can compile
music directly from the game (and probably half the existing
VVV_COMPILEMUSIC code is going to be thrown out, but oh well).
Since this refers to specific exported file data, let's make sure this
is portable. I'm not sure if we'll ever ship on systems where
sizeof(int) != 4 or sizeof(bool) != 1, but better to be safer and
future-proof than not.
This variable is only used when compiling music. Since it doesn't
actually keep track of the number of headers otherwise, ifdef it behind
VVV_COMPILEMUSIC.
2.3 introduced a regression with destroy(platforms). The problem was
that isplatform wasn't being set to false when the entity got disabled,
so if the platform was moving, it would keep moving until it hit a wall,
instead of stopping immediately.
Previously, loading STDIN used std::istreambuf_iterator and std::vector
and whatnot because... I guess it was less typing? But this isn't 1989;
we have the disk space to spare and we don't need to use fancy stuff
just to save on typing. It's not that hard to implement an array that
regrows to the nearest power of two every time.
All system header includes should come before project-specific includes
(includes specific to this game), while coming after the include
specific to the given file (if any; main.cpp doesn't have any).
These are unused.
Ethan originally added them in case Terry wanted achievement
percentages. But he didn't add them, and I don't think the achievements
are changing anytime soon, so it's safe to remove this dead code.
If the string is hardcoded, then use compile-time string literal
concatenation instead.
I don't know if compilers are smart enough to recognize when you're
passing in hardcoded strings and to concatenate them into the string
literal at compile time instead. I also don't know that if compilers are
smart enough to recognize that, that further they recognize all the
logging functions are just wrappers around printf, and so they can
perform the same optimization at those function call sites, too. So it's
better to just do the string concatenation explicitly instead.
Instead of having three separate copies of the function list, use macro
magic to make it so there is only one list that we use in three
different cases.
SDL just got an API to toggle VSync without having to tear down the
renderer ( libsdl-org/SDL#4157 ). We can remove the workaround and use
that instead. For now, we are putting it behind an ifdef until SDL
2.0.18 officially releases in November.
Fixes#831.
Constants.h will house constants like the screen size and others. But
basically only the screen size for now.
Now we don't have to type that "4 bytes per 40 chars (whole screen)"
comment everywhere...
Since those are all downstream recipients of either static storage or
memory that doesn't move for the duration of the custom level, it's okay
to make these be `const char*`s without having to redo any of the RAII
memory management.
mapclass::currentarea() is included in this as well. I also cleaned up
Tower.cpp's headers to fix some transitive includes because I was
removing UtilityClass.h includes from all other level files too.
The "Untitled room" names no longer show any coordinates, because doing
so would require complicated memory management that's completely
unneeded. No one will ever see them, and if they do they already know
they have a problem anyway. The only time they might be able to see them
is if they corrupted the areamap, but this was only possible in 2.2 and
previous by dying outside the room deaths array in Outside Dimension
VVVVVV, which has since been patched out. Besides, sometimes the
"Untitled room" gets overwritten by something else anyway (especially in
Finalclass.cpp), so it really, really doesn't matter.
There's no reason it needs to be an std::string here.
Although, realistically, we should be using an enum instead of
string-typing, but, eh, that can be fixed later.
Companions would not spawn if you didn't load the current room via a
room transition. This meant that companions wouldn't spawn if you loaded
a save file with a companion, at least not until you moved to a
different room and triggered a screen transition. But most importantly,
it meant that the Intermission 1 supercrewmate would never spawn,
because going to Intermission 1 does a straight gotoroom, and does not
do a room transition.
Turns out the roomchange refactor broke things, because of course it
did. The companion logic was implicitly relying on that bool to be set,
because...? Either way, it doesn't make sense. Using roomchange implied
that the code wanted to be ran only when doing a room transition, which
is clearly not the case here. The best thing to do here is to just move
it to a separate function that gets called at the end of
mapclass::gotoroom().
So, I ended up breaking supercrewmate spawning with that roomchange
refactor. However, upon investigating how to fix it, I was running into
a weird interpolation issue due to scmmoveme, as well as the companion
spawning in the ground in "Very Good". And I was wondering why I or no
one else ended up running into them.
Well, as it turns out, scmmoveme ends up doing absolutely nothing. There
are only two instances where scmmoveme is used. The first is if you
respawn in "Very Good", and somehow have your scmprogress set to that
room. But that's impossible, because whenever you respawn, your
scmprogress is always set to the one after the room you respawn in. Even
if you respawned in the room previous to "Very Good" (which is "Don't
Get Ahead of Yourself!"), it still wouldn't work, since the logic always
kicks in when a gotoroom happens, and not only when a supercrewmate is
actually spawned. Since the scmprogress doesn't match, that case never
gets triggered, and we get to the second time scmmoveme is used, which
is in the catch-all case that always executes.
This second instance... also does nothing, because since we just
respawned, and our scmprogress got set to the room ahead of us, there is
no supercrewmate on screen. Then getscm() returns 0, and the player is
always indice 0, so the only thing we end up doing is setting the
player's x-position to their own x-position. Brilliant.
Anyway, this code results in interpolation issues and the supercrewmate
spawning in the ground on "Very Good" if you die, when my fix is
applied, because my fix moves this logic around to a different frame
order, and that actually ends up making scmmoveme no longer dead code.
So to recap: we have dead code, which looks like it does something, but
doesn't. But if you move it around in a certain way, it ends up having
harmful effects. One of the joys of working on this game...
It's also hilarious that it gets saved to the save file. Why? The only
time this variable is true, it is for literally less than a frame,
because it always gets set to false, because you always respawn using a
gotoroom whenever the supercrewmate dies, because you never respawn in
the same room as a supercrewmate, because Intermission 1 was
deliberately designed that way (else you'd keep continually dying since
the supercrewmate wouldn't move out of the way).
These were bfont_rect, bg_rect, foot_rect, and images_rect.
bg_rect was only used once to draw the ghost buffer in the editor, but
that was only because Ally didn't know you could just pass NULL in, cuz
the ghost buffer is the same size as the backbuffer.
RGBflip() does the exact same thing as getRGB(), now that all the
surface masks have been fixed. This axes RGBflip() and changes all
callers to use getRGB() instead. It is more readable that way.
By doing this, there is less copy-pasting. Additionally, it is now
easier to search for RGBf() - which is an ENTIRELY different function
than RGBflip() - now that the name of RGBf is no longer the first four
characters of some different, unrelated function. Previously I would've
had to do `rg 'RGBf[^\w]'` which was stupid and awful and I hated it.
Turns out, the r, g, and b arguments don't actually do anything!
There was a call to RGBf() in the function. RGBf() is just getRGB() but
first adds 128 and then divides by 3 to each of the color channels
beforehand. Unfortunately, RGBf() does not have any side effects, and
the function threw away the return value. Bravo.
This also reveals that the website images drawn in the credits in the
main menu are only recolored because of a stale `ct` set by the previous
graphics.bigprint(), and not because any color values were passed in to
drawimagecol()... What fun surprises the game has in store for me every
day.
This fixes a regression where entering playtesting while a track was
fading out (by exiting out of playtesting with a track playing and then
immediately entering back in with the level start music set) would
result in no music.
The cause is the game doing fades even though nothing is playing, which
puts it in a confusing state.
This wrapper function is for (a) future-proofing (b) proactive
prevention of future copy-pasting (c) to clarify that we never actually
halt music in the SDL_mixer sense, we only pause it, so to check if the
music is halted we actually check if the music is paused instead. This
is important because Mix_PlayingMusic() does not check if the music is
paused and Mix_PausedMusic() does not check if the music is halted.
When you're on the music changing screen in the editor, it plays the
current track. When you return, it stops playing the track. However, if
you press escape, it doesn't stop playing the track. This is because
pressing escape just returns to the previous menu without stopping
playing the track.
To fix this, I just added some kludge in the return menu function. This
is kinda super bad but it works for now and is just something to clean
up later. Maybe like each menu having exit callbacks or something, I
dunno.
This is kinda a regression, kinda sorta not. In 2.2 and previous,
pressing escape would just close the settings menu entirely, which also
bypassed the music fadeout. 2.3 made it so pressing escape doesn't
entirely close the settings menu, and just returns to the previous menu,
which fails in a different way. But the intended way is definitely to
select the return option and having the music fade out.
This function now properly deletes the Super Gravitron record, the Super
Gravitron rank, and the best game deaths. They were not being properly
reset previously, meaning you would have to go into your save file to
properly clean out your save data.
If the map size was less than 20x20, platv values outside the map would
end up being saved as 67372036.
This happens because SDL_memset() operates on the byte level, and not
the multi-byte level. So it takes only the lower 8 bits of 4 and repeats
it for each byte in each integer, creating 67372036.
This was done in 2.2 and previous probably to fix the fact that there
were multiple conflicting audio controls (the player wants to mute the
audio but the game wants to fade in the audio), but is now actively
harmful since 2.3, because muting the game while finishing the
completion prompt means the music will never come back in, even after
unmuting.
I also notice that when collecting a custom crewmate, the game checks
for the level's start music instead of if there's actually a current
song playing right now. I don't know why this was done, because it
would've been better to copy-paste the trinket collection logic here.
It's entirely possible for the audio to just be muted and never come
back if the level has no start music but plays a song by using a script.
Anyways, leaving it alone because it's quite possible that a level might
be intentionally designed around this, I can't really tell the
intentions of every level creator, and it's easy to work around (either
don't use custom crewmates, which every modern level basically does
nowadays, or just set the start music).
For some reason, when completing a custom level and fading to the menu,
the game attempts to fade the music in and also fade the music out at
the same time. This results in nothing happening at all, and in 2.2 and
previous, results in audio fading out from max volume while the game is
frozen on a black screen after the fadeout.
To avoid any potential badness, just remove these.
In the main game, if you press R during the trinket collection prompt
after collecting a trinket, AND you have never entered Comms Relay, and
you respawn in a different room, the trinket collection gamestate will
be interrupted, but you will still be left with the advance text prompt,
cutscene bars, and muted music.
The previous workaround to fix the music would be to mute and then
unmute the game, but due to the new music changes, this workaround
(which in and of itself is a bug) no longer works. Instead, the music
would have to be restarted by going into another zone on the map.
Having an advance text prompt outside of a cutscene results in the
player being unable to flip, but they can still move around left and
right.
Speedrunners previously used the no-Comms-Relay interrupting behavior to
skip certain trinket collection prompts entirely with a frame-perfect R
press, so I can't patch that out. Having an advance text prompt outside
of a cutscene is (ab)used in custom levels to intentionally prevent the
player from flipping, and furthermore, it's also used in credits warp
runs of the main game to increment the gamestate; so I cannot patch that
out. The ability to press R everywhere even during cutscenes was added
for good reason - to make it less likely that a softlock can happen - so
I don't want to revert it.
But I still think this is worth fixing because previously, the
punishment for missing the frame-perfect window late was simply not
skipping the trinket prompt (since the R-press would be ignored), but
now the punishment is basically having to reset because of the advance
text prompt.
I would usually handle this in gamestate 0, but awful custom levels
might want to intentionally interrupt the gamestate to do, I don't know,
something. No level does that so far, but I'd like to do the least
invasive thing.
So what I've done is made it so the effects of interruption are undone
if you press R and the gamestate is interrupted. This is handled in
mapclass::resetplayer().
Without this, `fixedloop` will loop infinitely until focus is regained.
However, Emscripten won't actually know that focus is regained until
`fixedloop` returns.
getBGR, when used in FillRect, was actually passing colors in RGB order.
But now the masks are fixed, so remove it, and fix up all existing
getBGR colors to use getRGB instead.
Due to the mask inconsistencies, getRGB calls that were passed to
FillRect ended up actually being passed in BGR order. But now that the
masks are fixed, all these BGR colors look wrong. So, fix up all of them
(...that's a _lot_ of copy-pasted code...) to be passed in RGB order.
This fixes the color ordering of every SDL_Surface in the game.
Basically, images need to be loaded in ABGR format (except if they don't
have alpha, then you use RGB? I'm not sure what's going on here), and
then they will be converted to RGB/RGBA afterwards.
Due to the surfaces actually being BGR/BGRA, the game used to use
getRGBA/getRGB to swap the colors back around to BGRA/BGR, but I've
fixed those too.
If it's at all possible to use `const std::string&` when passing
`std::string`s around, then we use it. This is to limit the amount of
memory usage as a result of the frequent use of `std::string`s, so the
game no longer unnecessarily copies strings when it doesn't need to.
I've made a new function, Graphics::do_print(), that does the actual
text printing itself. All the interfaces of the other functions have
been left alone, but now just call do_print() instead.
I also removed PrintOffAlpha() and just calculated the center x-position
in bprintalpha() itself (like bigbprint() does) to make it easier to
de-duplicate code.
Text boxes have `r`, `g`, and `b`, and `tr`, `tg`, and `tb`. `tr`, `tg`,
and `tb` are the real colors of the text box, and `r`, `g`, and `b` are
merely the colors of the text box as the text box's alpha value is
applied to them.
Compare this with, say, activity zones (which are drawn like text boxes
but aren't text boxes): There is `activity_r`, `activity_g`, and
`activity_b`, and when they're drawn they're all multiplied by
`act_alpha`.
So just do the same thing here. Ditch the `tr`, `tg`, and `tb`
variables, and make `r`, `g`, and `b` the new `tr`, `tg`, and `tb`
variables. That way, there's simply less state to have to update
separately. So we can get rid of `textboxclass::setcol()` as well.
This is a variable that's only used in one method, and it's always
initialized beforehand. No need to carry it around, taking up memory,
and making code analysis more complicated.
All parameters are now made const, to aid in the reader in knowing that
they aren't ever changed.
Useless comments have been removed and been replaced with helpful
comments.
Useless parentheses have been removed.
Spacing has been made consistent.
Declarations and code are no longer mixed.
I'm honestly not too sure why drawcustompixeltextbox ever existed? All
it seemed to do was draw even more horizontal/vertical tiles to finish
any gaps in the tiling... which was all completely unnecessary and
wasteful, because even the previous drawpixeltextbox implementation
covered all gaps in all custom level map sizes that I tried.
Anyway, that at least gets rid of one copy-pasted function.
This draws the remaining horizontal/vertical tile just beside the final
corner if the width/height is not a multiple of 8. (It'd be wasteful to
draw it if the width/height was a perfect multiple of 8, and result in
double-drawing translucent pixels if there were any.)
This has an advantage over the previous system of shifting the
horizontal/vertical tiling, in that custom corner textures don't look
weird due to overlapping like this. Now, custom horizontal/vertical
tiles _can_ look weird if they don't completely tile correctly (or if
they have translucent pixels), but that's better than mucking up the
corners.
`w` and `h` are provided alongside `w2` and `h2`. `w2` and `h2` are in
blocks of 8, while `w` and `h` are in pixels. Therefore, `w2` and `h2`
can just be figured out by diving `w` and `h` by 8.
Also, `xo` and `yo` were used to slide the horizontal/vertical tiling of
the text box a bit into one set of corners, so the horizontal/vertical
tiling wouldn't visibly overlap with the other corners, if using default
textures. This requires hardcoding it for each width/height of text box,
which isn't something that's generalizable. Also, it results in corners
that look weird if the corners have custom textures that don't adhere to
the same shape as default textures.
In the next commit I'll fix the non-multiple-of-8 text box dimensions
differently. Can't do it in this commit or the diff looks weird (at
least with my diff algorithm).
This fixes a bug where the player could bring up the map on the very
first frame of a gamemode(game) animation. This is because the menu
animation checked graphics.menuoffset, but graphics.menuoffset wouldn't
have changed at that point because it only set graphics.resumegamemode.
Instead, just check for graphics.resumegamemode directly. We also need
to assign it to false whenever the map is closed so the player won't be
prevented from using the map screen again.
This fixes all the headaches about map.extrarow having to be the correct
value and which way it should be and whatnot. The latest headache was
the detection that prevent user-initiated menu animations while an
animation was already happening being tripped because
graphics.menuoffset would be 230 (due to closing the menu while being in
a room without a room name), but then going to a room with a room name
would check for 240 instead, and 230 is less than 240. (The numbers are
the wrong way round because I got the ternaries the wrong way round, but
even if the numbers are the correct way round, the bug would still
happen, but it would just be reversed.)
So instead, I've just made it 240 for both. This doesn't change the
duration of the menu animation (because the animation moves in
increments of 25, and 230 / 25 == 240 / 25 under integer division). It
might change the animation slightly, but it was already inconsistent
anyway because map.extrarow was always set to be 1 in custom levels, and
I legitimately would not be able to tell the difference without
recording the animations and nitpicking it frame-by-frame.
Fixes#841.
The player gets kicked out of the Super Gravitron if they have
invincibility or slowdown enabled. However, this can be confusing if no
message pops up
( https://steamcommunity.com/app/70300/discussions/0/3039355280230178910/ )
. So I've made it so that a text box will pop up when they get kicked
out.
This makes it so gamemode(teleporter) will always do an animation, even
if the game is already in TELEPORTERMODE.
I used this script to test:
gamemode(teleporter)
delay(5)
gamemode(teleporter)
delay(5)
gamemode(teleporter)
In 2.2, this script starts the map menu bringing-up animation three
times.
In previous 2.3, this script starts the map menu bringing-up animation
once, but then the next gamemode(teleporter) immediately finishes the
animation, and the third gamemode(teleporter) does nothing.
This commit restores it to 2.2 behavior.
This makes it so it's not even possible to stay on the TELEPORTERMODE
screen by opening the map while it's being brought down. It also makes
it so the map animation is able to be canceled when being brought up
just by opening the map and closing it.
Fixes#833.
This restores it to 2.2 behavior, where the cutscene bars timer also
ticked in TELEPORTERMODE. It was a 2.3 regression that the cutscene bars
timer didn't tick there.
This makes it so if you manage to get stuck in TELEPORTERMODE when a
cutscene ends, the cutscene won't be stuck on untilbars() waiting for
the cutscene bars to go away, since the cutscene bars timer now ticks.
Also, add a sync parameter to avoid calling syncfs too often.
Calling syncfs twice in a row is both inefficient and leads to errors
displaying twice. This allows us to bypass it when saving unlock.vvv as
part of savestatsandsettings.
This object basically had no reason to exist... it was just more verbose
to use, which really reminded me of Java. Anyway, this is the last thing
named after the editor for no reason when it should be a part of the
customlevelclass, so I moved its attributes to customlevelclass.
This fixes the fact that the name of the singular type is plural, but
the name of the plural array is singular. Which has always annoyed me,
too. Also this makes it more clear that custom entities don't have much
to do with the editor.
That's what it is - it's an entity in a custom level. Not something to
do with the editor, necessarily. Like before, the name of the XML
element will remain the same.
That's what edlevelclass is... so that's what it should be named. (Also
removes that "ed", too, making this less coupled to the in-game editor.)
Unfortunately, for compatibility reasons, the name of the XML element
will still remain the same.
CustomLevels.h now uses 4-space indents - like all other space-indented
files - instead of 2-space indents. This has bugged me for a while and I
decided to just fix it now.
This is a pretty hefty commit! But essentially, I made a new editorclass
object, and moved all functions and variables that only get used in the
in-game level editor to that class. This cleanly demarcates which things
are in the editor and which things are just general custom level stuff.
Then I fixed up all the callers. I also fixed up some NO_CUSTOM_LEVELS
and NO_EDITOR ifdefs, too, in several places.
As far as I can tell, this function has never been implemented, and only
existed in this header file. FILESYSTEM_getLevelDirFileNames() already
exists (well, used to exist; it's been changed and renamed to
FILESYSTEM_enumerateLevelDirFileNames()), so I'm removing this now.
This accompanies the editor.cpp -> CustomLevels.cpp change; I'll be
splitting out the editor functions in the next commit. The name of the
include guard has been changed as well, but not anything else.
This moves editorrenderfixed(), editorrender(), editorinput(),
editorlogic(), and their associated functions to a new file named
Editor.cpp - which is exactly what it says on the tin; it stores all the
functions related to the actual in-game editor loop. Also, the existing
editor.cpp has been renamed to CustomLevels.cpp.
All XML functions now check the return value of
tinyxml2::XMLDocument::Error() after each document gets loaded in to
TinyXML-2. If there's an error, then all functions return. This isn't
strictly necessary, but printing the error message that TinyXML-2 is the
bare minimum we could do to be useful.
Additionally, I've standardized the error messages of missing or
corrupted XML files.
Also, the way the game went about making the XML handles was... a bit
roundabout. There were two XML handles, one for the document and one for
the root element - although only one XML handle suffices. So I've
cleaned that up too.
I could've gone further and added error checking for a whole bunch of
things (e.g. missing elements, missing attributes), but this is good
enough.
Also, if unlock.vvv or settings.vvv don't exist yet, the game is
guaranteed to no-op instead of continuing with the function. Nothing bad
seems to happen if the function continues, but the return statements
should be there anyway to clearly indicate intent.
If settings.vvv doesn't exist, loadsettings() calls savesettings(), but
savesettings() already prints a message if settings.vvv doesn't exist.
So then the output would look like
No settings.vvv found. Creating new file
No settings.vvv found
Which is clearly redundant.
The same thing happens with unlock.vvv, but in that case the following
prints instead
No unlock.vvv found. Creating new file
No Stats found. Assuming a new player
I will need to be able to return from this function if there's an XML
error, otherwise writing out the control flow manually gets really
nasty. And while I'm at it, it's some a nice de-duplication as well.
To do this, we create a temporary struct that bundles up all the
information we want for the summary, and pass it in to the intermediate
load function.
Furthermore, we can get rid of reading map.finalstretch - it affects
nothing. map.finalmode is still needed, however, because of the usage of
map.area().
Previously, Flip Mode rendering had to be complicated and allocate
another buffer to call FlipSurfaceVerticle, and it was just a mess.
Instead, why not just do SDL_RenderCopyEx, and let SDL flip the screen
for us? This ends up pretty massively simplifying the rendering code.
`-forcecolor` will force color to be on. `-nocolor` will force color to
be off.
And just because I'm a nice person, I've also added British versions of
those flags. As a treat.
This includes the bold as well.
INFO is just default, WARN is yellow, ERROR is red.
We try to automatically detect if the output is a TTY (and thus supports
colors), and don't emit colors if so. Windows 10 supports ANSI color
codes starting with a specific build, but we don't care to emit whatever
garbage Microsoft invented for builds older than that.
This is because the y-position of the graphics.onscreen() check was a
little too high. Then their name (under Beta Testing) would suddenly
disappear too early. You'd have to look real close to spot it, but it
does happen. It's cuz the credits are all kinda hardcoded, which is
probably bad, but fixing that would have to come later...
I talked with Ethan earlier about this. For 2.3, he wanted me in GitHub
contributors (well, still separate from the rest), to really highlight
the source-code-release community-driven nature of 2.3, but he said it'd
be fine to put me in C++ credits in 2.4.
The RWops stuff isn't a part of any standard PhysFS package (and given
that it explicitly wraps around SDL I'm not sure how you _would_ package
it). So we need to get the physfsrwops.h include in if
BUNDLE_DEPENDENCIES is off, otherwise this results in a compile-time
include-not-found failure.
Additionally, I've placed the PhysFS RWops stuff in their own extras/
folder, so none of the other PhysFS stuff gets included in a
-DBUNDLE_DEPENDENCIES=OFF build.
The game will freeze the player immediately if they release a
directional button within 3 frames of pressing it. Similar to flipping,
this involves global state, and will only apply to the first player
entity.
Closes#484
Flipping only applies momentum to the player entity currently being
processed. This normally wouldn't be a problem. However, flipping
involves global state, and only one flip can occur per frame. This means
that additional player entities don't get this boost of momentum, which
feels somewhat unnatural during gameplay.
This commit fixes this by splitting flip logic out of the loop over
player entities, and applying the flip momentum to all player entities.
We need to check for graphics.setflipmode, not graphics.flipmode,
because graphics.flipmode only gets assigned at the end of the frame
(due to the deferred callback). Otherwise, returning from the options
menu would always turn flag 73 on, which would make you ineligible to
get the Flip Mode trophy, even if you're in Flip Mode.
Originally this started as a "deduplicate a bunch of duplicated code in script commands" PR,
but as I was working on that, I discovered there's a lot more that needs to be done than
just deduplication.
Anything which needs a crewmate entity now calls `getcrewmanfromname(name)`, and anything which
just needs the crewmate's color calls `getcolorfromname(name)`. This was done to make sure that
everything works consistently and no copy/pasting is required. Next is the fallback; instead of
giving up and doing various things when it can't find a specific color, it now attempts to treat
the color name as an ID, and if it can't then it returns -1, where each individual command handles
that return value. This means we can keep around AEM -- a bug used in custom levels -- by not
doing anything with the return value if it's -1.
Also, for some reason, there were two `crewcolour` functions, so I stripped out the one in
entityclass and left (and modified) the one in the graphics class, since the graphics class also
has the `crewcolourreal` function.
If `setactivitytext` was the last line in a script,
the command would index the vector out of bounds.
I also modified the formatting to keep consistent
with the rest of the codebase.
These commands will change the colour and text of the next
activity zone that gets spawned. `setactivitycolour` takes all
textbox colors, and `setactivitytext` will take the text on
the next line. These commands were designed this way
to avoid breaking forwards compatibility.
When an activity zone is spawned through the
use of `createactivityzone`, and `i` is 35,
then it'll change the activity zone text to
"Press ENTER to interact".
On Emscripten, SDL_Delay is implemented as a busy loop. In addition,
everything happens on a single thread. This effectively means that
you have to let Emscripten manage the main loop, since if you do it
yourself the browser will just be frozen.
Otherwise, the new arguments to destroy(), which are 'moving' and
'disappear', would be thrown away by the simplified parser. Let's create
less work for ourselves to do and simply not have a hardcoded list of
allowed arguments for destroy() in the parser.
destroy(platforms) has been bugged since 2.0. The problem with it is
that it removes the platform entity, but doesn't remove its block. This
results in essentially turning the platorm invisible and stopping it
from moving.
This error should be fixed, but some levels (including my own) rely on
the invisible platform trick. So instead, the fixed version will be
implemented under a different name, destroy(moving).
There's also another problem with destroy(platforms), which is that the
name is misleading and it doesn't additionally destroy disappearing
platforms. I would also fix this, but in order to not run the risk of
breakage, it will have to be implemented under a different name, too. So
this will be destroy(disappear). As an added benefit, it's also more
granular to have platform-destroying functions under different names
than it is to consolidate them under the same name.
When I added the two-frame delay fix, I didn't realize that Game had a
roomchange variable that was being used as a temporary variable here.
Now that it's fully spelled out and obvious (just look at the top of
gamelogic()), I realize that the variable exists and is being used, and
other readers will realize it's being used too - so now that I know it
exists, I can axe the screen_transition variable I added in favor of
using roomchange instead.
The purpose of this variable was to keep track of if gamelogic() called
map.gotoroom() at any point during its execution. So map.gotoroom()
always unconditionally set it to true, and then gamelogic() would check
it later.
Well, there's no need to put that in a global variable and do it like
that! It makes it less clear when you do that.
So what I've done instead is made a temporary macro wrapper around
map.gotoroom() that also sets roomchange to true. I've also made it so
any attempt to use map.gotoroom() directly results in failure (and since
then using map.gotoroom() in the wrapper macro would also fail, I've had
to make a gotoroom wrapper function around map.gotoroom() so the wrapper
macro itself doesn't fail).
This is a temporary vector that only gets used in mapclass::gotoroom().
It's always guaranteed to be cleared, so it's safe to move it off.
I'm fine with using references here because, like, it's a C++ STL vector
anyway - when we switch away from the STL (which is a precondition for
moving to C), we'll be passing around raw pointers here instead, and
won't be using references here anyway.
This is a temporary variable that doesn't need to be on Game. It is
guaranteed to be initialized every time mapclass::gotoroom() gets
called, so it's safe to move it off.
Enemy/platform bounds are intended to not be drawn if they cover the
whole screen, since that's what their default bounds are.
However, the code inadvertently made it so if ANY of the bounds touched
a screen edge, the bounds wouldn't be drawn. This is because the
conditionals used "and"s instead of "or"s. The proper way to write the
positive conditional is "x1 is 0 and y1 is 0 and x2 is 320 and y2 is
240", and when you invert that conditional, you need to also invert all
"and"s to be "or"s. This is not the first time that the game developers
failed to properly negate conjunctional statements...
This is to make it so RNG is deterministic when played back with the
same inputs in a libTAS movie even if screen effects or backgrounds are
disabled.
That way, Gravitron RNG is on its own system (seeded in hardreset()),
separate from the constant fRandom() calls that go to visual systems and
don't do anything of actual consequence.
The seed is based off of SDL_GetTicks(), so RTA runners don't get the
same Gravitron RNG every time. This also paves the way for a future
in-built input-based recording system, which now only has to save the
seed for a given recording in order for it to play back
deterministically.
Otherwise, levels could leave stale arguments in the array, and then the
behavior of another level loaded right after might end up being
different because of that.
This is done for consistency with Terry's patrons, which are sorted by
first name and not last.
Also some people go with their usernames and so don't have a last name
to speak of, which ended up being pretty weird.
Kai is my last name. Elizabeth is my middle name. I went with my middle
name as last name for a while before figuring out what I wanted my last
name to be.
Third time's the charm.
The fundamental problem with the previous attempts was that they ended
up saying arguments existed due to stale `words` anyway. So to actually
know if an argument exists or not, we need to assign to `argexists` _as_
we parse the line.
And make sure to take care of that last argument too.
Also I thoroughly tested this this time around. I'm done pulling my hair
out over this.
Ever since tilesheets got expanded, custom levels could use as many
tiles as they wanted, as long as it fit under the 32-bit signed integer
limit.
Until 6c85fae339 happened and they were
reduced to 32,767 tiles.
So I'm being generous again and changing the type of the contents array
(in mapclass and editorclass) back to int. This won't affect the
existing tilemaps of the main game, they'll still stay short arrays. But
it means level makers can use 2 billion tiles once again.
This lets users place down tiles above 1199 in Direct Mode, if their
tilesheet has more than 1200 tiles.
I don't like the copy-pasted code here but it'll have to make do.
If you use Lab tilecol 6, you get the rainbow background. However, this
is unintended, because the associated autotiling is... not very good.
To combat that, Ved disallows using the Lab rainbow background outside
of Direct Mode. We will follow Ved here and only allow switching to the
rainbow background if you're in Direct Mode. Also make sure if someone
is disabling Direct Mode with the rainbow background that it gets reset
properly.
The main game used a set of copy-pasted code to set the music of each
area. There WAS some redundancy built-in, but only three rooms in each
direction from the entrance of a zone.
Given this, it's completely possible for players to mismatch the music
of the area and level. In fact, it's easy to do it even on accident,
especially since 2.3 now lets you quicksave and quit during cutscenes.
Just play a cutscene that has Pause music, then quicksave, quit, and
reload. Also some other accidental ways that I've forgotten about.
To fix this, I've done what mapclass has and made an areamap. Except for
music. This map is the map of the track number of every single room,
except for three special cases: -1 for do nothing and don't change music
(usually because multiple different tracks can be played in this room),
-2 for Tower music (needs to be track 2 or 9 depending on Flip Mode),
and -3 for the start of Space Station 2 (track 1 in time trials, track 4
otherwise).
I've thoroughly tested this areamap by playing through the game and
entering every single room. Additionally I've also thoroughly tested all
special cases (entering the Ship through the teleporter or main
entrance, using the Ship's jukebox, the Tower in Flip Mode and regular
mode, and the start of Space Station 2 in time trial and in regular
mode).
Closes#449.
2.3 has a regression where if you move back and forth between a zone,
you can get the wrong music playing in a zone. An example is the
Overworld and Lab. Just walk in to the Lab and immediately walk back
out, and you'll get Potential for Anything playing in the Overworld.
This regression was caused by facb079b35.
That commit removed assigning -1 to currentsong when a fadeout was
called.
Basically, the previous behavior was: currentsong is 4, we enter Lab and
nicechange gets queued to 3 but currentsong gets set to -1, then going
back nicechange gets queued to 4 again.
However, if we don't assign -1, then going back will keep nicechange at
3. Why? Because niceplay() checks for currentsong before assigning
nicechange. If currentsong is still the same then it doesn't assign
nicechange.
To fix this, just always unconditionally assign nicechange.
If spawned as a custom enemy (createentity entry 56), or spawned outside
of the rooms they spawn in in the main game, they will repeatedly clone
themselves every frame, which profusely leaks memory. In fact it quickly
causes a crash in 2.2 and previous, but 2.3 fixes that crash, so it just
keeps spawning enemies endlessly, which eventually lags the game, and
eventually can out-of-memory your system (bad!).
The problem is those movement types rely on entclass::setenemyroom() to
change their `behave` to be 11 or 13. Else, the new entity created will
still have `behave` 10 or 12, which will create ANOTHER entity in the
same way, and so on, and so forth.
So to fix this, just make it so if an enemy is still `behave` 10 or 12
by the end, then, just set it to -1. That way it'll stay still and won't
cause any harm.
I considered setting the `behave` to 11 or 13 respectively, but, that's
probably going farther than just fixing a memory leak, and anyways, it's
not that much useful for me as a custom level maker, and the entities
spawned aren't really controllable.
In order to let callers provide their OWN callback functions through the
callback function WE provide to PhysFS, we casted the function pointer
to a void pointer.
Unfortunately, this is apparently undefined behavior... if your compiler
doesn't have an extension for it. And most compilers on most
architectures do. (In fact compilers on POSIX systems most certainly
have it due to dlsym() returning a void* which could actually be a
pointer to a function sometimes.)
But imo, it's better to be safe than sorry in this regard. Especially
when given GCC's approach to optimizing int + 100 > int (spoilers: they
remove it entirely! It's faster, but also broken!).
I've decided to wrap it in a struct. And as a nice side effect, if we
ever need more data to be passed through... well we already have this
struct.
Technically, it's also standards-compliant to cast a _pointer to_ a
function pointer to a void pointer. But that extra layer of pointer
indirection would get real confusing to conceptualize real fast (or at
least is more confusing than just putting it in a struct).
Since you've been able to resume music stopped by stopmusic() with
resumemusic(), if a track was stopped by stopmusic(), the unfocus pause
itself would end up resuming the track when regaining focus.
The solution is to simply check for if music.currentsong is -1 or not.
So, platv is a room property that controls the speed of custom entity
platforms in the room (unless, of course, they're created with
createentity). Problem is, this is how 2.2-and-previous coding standards
were:
ed.level[game.roomx-100+((game.roomy-100)*ed.maxwidth)]
Overly long, verbose, not entirely clear unless you already know what it
means? Copy-pasted over and over due to all of the above? Surely a
recipe for not making any coding errors!
Ironically enough, copy-pasting is basically the best approach here
(short of refactoring the whole thing, like I did in
945d5f244a), since if you don't ACTUALLY
copy-paste and just re-type it on your own, you'll end up making more
mistakes. Like what happened here:
ed.level[game.roomx-100+((game.roomy-100)*ed.mapwidth)].platv
Do you see the mistake...? Yeah, mapwidth (with a P) instead of maxwidth
(with an X). You'd have to look closely to find it.
So what does this mean for platv? Well, it means that it multiples the
y-coordinate of the room by the map width instead of the max width (20),
like every other room property. So that means if your map width is less
than 20, like say, map width 10, the platv value for (2,2) will be
stored in (2,1)'s room properties instead of (2,2)'s. Because if you go
off of map width, the room index for (2,2) is 2 + 2 * 10 = 22, but if
you go off of max width, the room index for (2,1) is 2 + 1 * 20 = 22.
Now this wouldn't be bad, except for another 2.2-and-previous
standard... kind of just not exposing things directly to the end user.
Whether that's simply not documenting something (as in the case of
ifwarp and warpdir, which by all measures were completely intended to be
used in custom levels but just simply were never known properly until I
discovered how to use them in 2019), or in this case, not giving any way
for the user to fiddle with platv from the in-game editor. Because if
there was a way to do that, and someone decided to test to see if platv
worked okay, they would discover something was up.
So... since I refactored room properties in
945d5f244a, I kind of broke platv by
fixing it. Now levels that relied on platv being the broken way don't
work.
How do I fix it, and thus break it again? Well, I'll do what I did for
scripts - handle the scrambling when reading and writing the level, and
keep things sane at least internally.
Thus: editorclass::load() will unscramble platv data in the right way,
and editorclass::save() will re-scramble platv in the right way too.
To match the option to nuke all main game save data, there is also now
an option to nuke all custom level save data separately (which is just
all custom level quicksaves, along with stars for level completion). It
has its own confirmation menu too. It does not delete any levels from
the levels folder.
Custom level quicksaves are NOT affected by the clear data menu, so the
player should be able to delete quicksaves this way. The quicksave
confirmation menu now has an extra option to delete the save (and that
option also has its own confirmation menu before deleting).
This error case can happen, but if it does, non-console users get an
ERROR page with no further information. So use setLevelDirError if this
failure mode happens. And Menu::errorloadinglevel needs to be changed to
accomodate that.
Not sure why the original implementation decided to do things this way
instead of snprintf'ing a path to the .zip itself. Otherwise, if the
level is from data.zip, PHYSFS_getRealDir() will return the path of
data.zip, which then fails to mount for separate reasons.
Since createentity() started accepting p1/p2/p3/p4 arguments, it now
unconditionally passes in whatever arguments were present there
previously, when there weren't any before.
This can lead to unexpected behavior when selectively using and then
omitting p1/p2/p3/p4 arguments.
Also, plenty of existing levels already only use the 5-argument version
of createentity(). And createcrewman() can take up to 6 arguments at
once. It's not far-fetched that an existing level could createentity()
right after doing a 6-argument createcrewman(), which would lead to a
different behavior than in 2.2 and previous.
So instead, instead of checking if `words[index]` is an empty string (it
only sets the string to be empty if there are enough argument separators
on the line), ACTUALLY check if it's empty. I've added a static array
(no need for it to be exported) that keeps track of this. createentity()
now checks for that instead of `words`.
It's possible to get one page of levels by removing all the built-ins,
either by removing them directly from data.zip or by putting files with
the same filenames as them in your level folder that don't contain
nothing.
And hey, there's already a check for if no levels exist at all, so why
not check for this too?
Previously, you would only get the trinket completion star if you got
the exact same amount of trinkets as there are custom entity trinkets in
the level file. But if you got more (say, if the level spawned extra
"bonus trinkets"), you wouldn't be able to get the star.
This is true of the custom crewmate case as well, but I've decided to
not change that case, because there are still downsides to the resulting
behavior and it's better to just leave it alone because it's rare for it
to happen anyways.
Since custom levels have gained the functionality to show trinkets on
the minimap, it's nice to just save the showtrinkets variable directly
to the save file, without having to make level makers handle it
themselves.
If you have unfocus pause off, and unfocus audio pause off, then this command will go into effect.
When it's set to on, the audio will pause when you unfocus the game. When it's set to off, the
audio will not. This is different from the setting, and gets saved to the save file.
If a zip file is improperly structured, a message will be displayed when
the player loads the level list.
This will only display the last-displayed improper zip, because there
only needs to be one displayed at a time. Also because doing anything
more would most likely require heap allocation, and I don't want to do
that.
This will wrap text on-the-fly, since I will be introducing text that
needs to be wrapped whose length we can't know in advance. (Or we can,
but, that'd be stupid.)
I took the algorithm from Dav999's localization branch, but it's not
like it's a complicated algorithm in the first place. Plus I think it
actually handles words that get too long to fit on a single line better
than his localization branch. The only difference is that I removed all
the STL, and made it more memory efficient (unlike his localization
branch, it does not copy the entire string to make a version with
newline separator characters).
This macro needs to be used because Clang is stupid and doesn't let you
use /* fallthrough */ comments like GCC does. However, if GCC is too old
(as is the case on CentOS 7), then it won't recognize __has_attribute
either.
Some people prefer the 2.2 behavior where unfocusing pauses the game,
but the music still plays. One such person is Trinket9 on the VVVVVV
Discord server, who wanted it that way.
The reason audio pausing was added in the first place was to prevent
desyncing music in levels with cutscenes that synced to music. Rather
than reverting it, let's add this option instead.
Similar to disabling the elephant flashiness, at least one
photosensitive person has told me the flashy color animation makes their
eyes kind of hurt a little bit. Also it screws up the compression really
badly when they record (especially the green noisy tiles!).
The colors will still cycle, but the individual animations within each
color will be completely static.
It's quite rude to close the game. Especially if the user does not use
the console. They won't know why the game closed.
Instead, just return -1. All usages of font_idx() should be and are
bounds checked anyways. This will result in missing characters, but,
it's not like the characters had a font image in the first place,
otherwise we wouldn't be here. And if the user sees a bunch of
characters missing in their font, they'll probably work out what the
problem is even without having a console. And it's still far better than
abruptly closing the game.
And use WHINE_ONCE to prevent spamming the console.
Let's say you have a zip named LEVELNAME.zip, but the only .vvvvvv file
it contains is NOTLEVELNAME.vvvvvv. This zip would end up printing both
the 'LEVELNAME.vvvvvv is missing' and 'It has .vvvvvv file(s) other than
LEVELNAME.vvvvvv' messages, even though we already know there's
something wrong with the zip, and the 'other level files' message is
redundant, since in this case the problem here is simply just the
.vvvvvv file being named the wrong way.
The 'other level files' message is only intended to be printed when
LEVELNAME.vvvvvv *does* exist, but there's additional .vvvvvv files in
the zip on top of that, so don't print this message if LEVELNAME.vvvvvv
exists.
Since colors going into FillRect() need to be in BGR format, we need to
use getBGR instead. (Well, actually, it gets passed in RGB, but then at
some point the order gets switched around, and, really, this game's
masks are all over the place, I'm going to fix that in 2.4.)
This can happen if you select an option in a menu that (A) returns to
the previous menu and (B) saves settings. If the settings save fails,
this will create another menu on the same frame that cycles the tower BG
after it's already been cycled for that frame. Examples are the slowdown
and glitchrunner menus.
I could fix this by creating a new function that copy-pastes all of
Game::savestatsandsettings_menu() except for the map.nexttowercolour()
at the end. But that's copy-pasting code.
Instead what I've done is added a variable to signal if the color has
already been cycled this frame, so we don't cycle it again. This also
covers cases of possible double-cycling in the future as well.
This is because the fade delay did not last long enough.
I was under the mistaken impression that the fade animation lasts for 15
frames. However, this does not account for the fact that the offset of
each fade bar is dependent on RNG, and the worst case scenario is that
they have an offset of 96 pixels (in the opposite direction of the
fade).
The actual fade animation timer accounts for the worst case scenario, so
the fade animation actually lasts for (320 pixels plus 96 pixels is 416
pixels, 416 pixels divided by 24 pixels per frame equals 17.333...
frames, but since the actual timer keeps adding/subtracting 24 pixels
per frame until it passes the 416-pixel threshold, that gets rounded up
to...) 18 frames.
And an extra frame to make it so deltaframe interpolation doesn't
suddenly stop on the last deltaframes before the screen is completely
black.
I also need to draw the screen black on the map screen when glitchrunner
mode is off, if there's a fadeout going on. Else that would introduce
yet another frame flicker.
This fixes a bug where the player would always be facing right if they
were loading in for the first time. This essentially made them always
ignore the facing direction set in the save file if the facing direction
was leftwards.
The problem is facing direction only gets set in map.resetplayer(), but
if loading in for the first time, that path is never taken (unless you
are loading a main game quicksave that's inside a tower). The solution
is to always reset the player, even after creating them for the first
time.
This fixes being able to re-trigger the fadeout while a fadeout is
already happening. It also fixes being able to enter playtesting during
the fadeout, which means the level now has a fadeout you normally can't
do in actual gameplay.
There's nothing to interpolate. It moves at one pixel per frame. And
interpolating sometimes results in the box being short by 1 pixel to
cover the whole screen on deltaframes, so if you stand on the right edge
of the screen and have a translucent sprite, it will quickly draw over
itself many times, and it looks glitchy. This commit fixes that bug.
Previously, turning glitchrunner mode on essentially locked you to
emulating 2.0, and turning it off just meant normal 2.3 behavior. But
what if you wanted 2.2 behavior instead? Well, that's what I had to ask
when a TAS of mine would desync in 2.3 because of the two-frame delay
fix (glitchrunner off), but would also desync because of 2.0 warp lines
(glitchrunner on).
What I've done is made it so there are three states to glitchrunner mode
now: 2.0 (previously just the "on" state), 2.2 (previously a state you
couldn't use), and "off". Furthermore, I made it an enum, so in case
future versions of the game patch out more glitches, we can add them to
the enum (and the only other thing we have to update is a lookup table
in GlitchrunnerMode.c). Also, 2.2 glitches exist in 2.0, so you'll want
to use GlitchrunnerMode_less_than_or_equal() to check glitchrunner
version.
Two problems: the fRandom() range was from 0..36, but that's 37
characters, not 36. And the check to sort the lower 26 values into the
Latin alphabet used a 'lesser-than-or-equal-to 26' check, even though
that checks for the range of values of 0..26, which is 27 letters, even
though the alphabet only has 26 letters. So just drop the equals sign
from that check.
It was checking for .vvv-mnt-temp-XXXXXX/LEVELNAME.vvvvvv instead of
LEVELNAME.vvvvvv. When PhysFS enumerates the folder, it only gives us
LEVELNAME.vvvvvv, and not .vvv-mnt-temp-XXXXXX/LEVELNAME.vvvvvv.
This fixes a regression that desyncs my Nova TAS after re-removing the
1-frame input delay.
Quick stopping is simply holding left/right but for less than 5 frames.
Viridian doesn't decelerate when you let go and they immediately stop in
place. (The code calls this tapping, but "quick stopping" is a better
name because you can immediately counter-strafe to stop yourself from
decelrating in the first place, and that works because of this same
code.)
So, the sequence of events in 2.2 and previous looks like this:
- gameinput()
- If quick stopping, set vx to 0
- gamerender()
- Change drawframe depending on vx
- gamelogic()
- Use drawframe for collision (whyyyyyyyyyyyyyyyyyyyyyyyyyyy)
And now (ignoring the intermediate period where the whole loop order was
wrong), the sequence of events in 2.3 looks like this:
- gamerenderfixed()
- Change drawframe depending on vx
- gamerender()
- gameinput()
- If quick stopping, set vx to 0
- gamelogic()
- Use drawframe for collision (my mind has become numb to pain)
So, this means that all the player movement stuff is completely the
same. Except their drawframe is going to be different.
Unfortunately, I had overlooked that gameinput() sets vx and that
animateentities() (in gamerenderfixed()) checks vx. Although, to be
fair, it's a pretty dumb decision to make collision detection be based
on the actual sprites' pixels themselves, instead of a hitbox, in the
first place, so you'd expect THAT to be the end of the dumb parade. Or
maybe you shouldn't, I don't know.
So, what's the solution?
What I've done here is added duplicates of framedelay, drawframe, and
walkingframe, for collision use only. They get updated in gamelogic(),
after gameinput(), which is after when vx could be set to 0.
I've kept the original framedelay, drawframe, and walkingframe around,
to keep the same visuals as closely as possible.
However, due to the removal of the input delay, whenever you quick stop,
your sprite will be wrong for just 1 frame - because when you let go of
the direction key, the game will set your vx to 0 and the logical
drawframe will update to reflect that, but the previous frame cannot
know in advance that you'll release the key on the next frame, and so
the visual drawframe will assume that you keep holding the key.
Whereas in 2.2 and below, when you release a direction key, the player's
position will only update to reflect that on the next frame, but the
current frame can immediately recognize that and update the drawframe
now, instead of retconning it later.
Basically the visual drawframe assumes that you keep holding the key,
and if you don't, then it takes on the value of the collision drawframe
anyway, so it's okay. And it's only visual, anyway - the collision
drawframe of the next frame (when you release the key) will be the same
as the drawframe of the frame you release the key in 2.2 and below.
But I really don't care to try and fix this for if you re-enable the
input delay because it's minor and it'd be more complicated.
In the past, people have reported having glitched levels where they
can't get the trinket star or can't complete the level because the
number of trinkets or crewmates is one higher than what can be obtained
in the level.
How did this happen? Well, it turns out that if you place an entity, and
then resize the level to be smaller, that entity still exists. This is
inconsequential for most entities, but if the entity is a trinket or
crewmate, that entity is still counted towards the number of trinkets or
crewmates in the level.
One fix would be to just remove entities whenever the level is
downsized, but then if someone accidentally downsizes the level and
wants to go back, that entity will be gone. Plus, it would be
inconsistent with tiles, because tiles don't get removed when you
downsize the level. Also, it wouldn't fix existing levels where people
have managed to place trinkets or crewmates out of bounds.
So instead, ed.numtrinkets() and ed.numcrewmates() should simply ignore
trinkets and crewmates that are outside the playable area. That way,
levels with glitched trinkets and crewmates can still be completed, and
can still be completed with the trinket star.
This fixes a regression where you're unable to activate activity zones
in in-editor playtesting if your interact button is not separate from
the map button.
When I originally did #743, I didn't have an option to set the bind to
be non-separate, so I removed this logic without adding a
game.separate_interact check. But when I added the option, I overlooked
this code, and so this regression happened. Whoops.
Not every music path will trip the quick_fade bool that resets the timer to
500ms, so we need to do this as soon as it's asked of us. This fixes the fade
when quitting to the main menu.
Fixes#764
Without this you end up with two problems:
- Fades will start past their fade time, causing it to just not fade at all
- Fades will start in the middle of their fade time, causing dramatic changes
in volume that are unintentional
The fade system already preserves the volume that music is playing during a
previous fade, so we can always reset the timer and get a good result.
Part of #764
This fixes one of two desyncs in my Nova TAS.
The problem is that by adding two frames of edge-flipping to vertically
moving platforms, Viridian's framedelay is updated for one extra frame
after they step off of a vertically-moving platform. This then messes up
Viridian's drawframe for the rest of the TAS until they die in a
drawframe-sensitive trick.
The solution here is to only set the visual onroof/onground to 1
instead. The logical onroof/onground is still 2, so players still have
two frames of edge-flipping off of vertically-moving platforms - it just
won't really look like it (not that you could easily tell anyway).
- use fseeko and ftello like FreeBSD in tinyxml2
- use current directory as basePath if NULL (OpenBSD doesn't actually support this feature it is disabled via a patch in their ports)
In order to help players spot the difference between outlined text and
non-outlined text, we now outline the text outline text itself (if text
outline is enabled, of course). But drawing the outline alone doesn't
stand out enough, so we have to draw a solid backing against the text as
well, in order to properly show the contrast.
This fixes a regression where you're able to start flipped by restarting
and then holding ACTION.
This happens because when the game resets all variables, it turns
hascontrol back on (because of hardreset()). However, this is handled in
the input function, and it's handled before player input is handled, so
the player is able to get 1 frame of being able to flip after a time
trial resets.
Why didn't this happen in 2.2? Because resetplayer() in 2.2 would set
lifeseq to 10, as if the player had died. However, this is inconsistent,
because loading in to the game for the first time would not result in a
lifeseq of 10. So, in 2.2, restarting the time trial would remove that 1
frame of being able to flip because of lifeseq, while 2.3 doesn't set
lifeseq because the player hasn't died.
I could have fixed this by setting lifeseq in the time trial restart
code, but I decided to just set hascontrol to false instead.
Fixes#770.
In earlier 2.3, if the roomname was empty, Dimension VVVVVV was used
instead. However, instead of doing that, it's better to just use the
hiddenname instead. Both because it's less hardcoded, and some rooms
have hidden names that aren't Dimension VVVVVV.
This makes the text much more readable against certain backgrounds (if
you have text outline enabled), especially against the Warp Zone
background (when you start in "This is how it is").
If you enter the Secret Lab from the title screen, all rooms will be
explored. However, if you enter the Secret Lab via the Secret Lab
entrance cutscene (epilogue), not all rooms will be explored, which is
inconsistent.
To do this, just do an SDL_memset() for the entersecretlab script
command.
SDL_memset() conveys intent better and is snappier than using a
for-loop. Also, using SDL_memset() to explore all rooms is more
future-proof, in case the size of map.explored were to change in the
future, and it's more conducive to optimization.
However, the `i` variable has to be explicitly set because it was
previously used here, but it's much better that it's explicitly set here
rather than being subtlely hidden in the inner for-loop initialization.
This is more future-proofing than anything else. The position of the
indicators is just the x-position of the gravitron square divided by 10,
but the gravitron squares will always only ever move at 7 pixels per
frame - so the distance an indicator travels on each frame will only
ever be at most 1 pixel. But just in case in the future gravitron
squares become faster than 10 pixels per frame, their indicators will be
interpolated as well.
When rollcredits is ran during in-editor playtesting, all unsaved data
is lost. To prevent this, just return to the editor if rollcredits is
ran, with a note saying "Rolled credits".
The text box drawn at the bottom of the map screen isn't wide enough, so
it's possible to see the corners on the right side of the text box if
you have custom graphics like I do.
The solution is to increase the width of the text box by one tile.
The game automatically writes settings to disk after any other setting
is changed, so it should do the same whenever the user changes
controller keybinds.
For consistency, the Viridian squeak will now play whenever you start
editing a level description field, or finish editing it (either by
pressing Esc or Enter).
If a level zip is named LEVELNAME.zip, the level file inside it must
also be named LEVELNAME.vvvvvv, else custom assets won't work.
This is because when we mount the zip file, we simply add
LEVELNAME.vvvvvv to the levels directory. Then whenever we load
LEVELNAME.vvvvvv, we look at the filename, remove the extension, and
look for the assets inside the zip of the same name, LEVELNAME.zip.
As a result, if someone were to make a level zip with assets but
mismatch the filename, the assets wouldn't load. Furthermore, if someone
were to add extra levels in the same zip, they wouldn't have any assets
load for them as well, which could be confusing.
To make things crystal-clear to the user, we now filter out any zips
that have incorrect structures like that, and print a message to the
terminal. Unfortunately nothing gets shown for non-terminal users, but
at least doing this and filtering out the zips is less confusing than
letting them through but with the issues mentioned above.
FILESYSTEM_mountAssets() has a big comment describing the magic numbers
needed to grab FILENAME from a string that looks like
"levels/FILENAME.vvvvvv".
Instead of doing that (and having to write a comment every time the
similar happens), I've written a macro (and helper function) instead
that does the same thing, but clearly conveys the intent.
I mean, just look at the diff. Using VVV_between() is much better than
having to read that comment, and the corresponding SDL_strlcpy().
This is so it can be reused without having to copy-paste.
generateBase36() is guaranateed to completely initialize and
null-terminate the buffer that is passed in.
This fixes a bug where the player's y-position would be incorrect if
they loaded a save that was on a conveyor and it was their first time
loading in since the game was opened.
This is because on the first load, the game creates a new player entity,
but on subsequent loads, the game re-uses the player entity. Subsequent
loads use mapclass::resetplayer(), which already has the newxp/newyp
fix, but as for the first time, the game does not set newxp/newyp.
So just set newxp/newyp, like in mapclass::resetplayer().
Upon further discussion it was decided to keep the soundtrack as originally
shipped, instead of changing it after the fact.
This reverts commit cf51379097.
There is a pattern in the Super Gravitron that is meant to "staircase",
similar to the Gravitron in Intermission 2. Something like:
[]
[]
[]
[] []
[] []
Unfortunately, due to an oversight, this pattern can only ever produce 1
square or 4 squares, which look out of place.
Both gravitrons are state machines (of course). States 20 and 21 in the
Super Gravitron are this staircase pattern (state 20 spawns the squares
on the left, state 21 spawns the squares on the right).
The only way states 20 and 21 can be reached is through state 1, and the
only way state 1 can be reached is through state 3. The only way state 3
can be reached is through states 28, 29, 30, and 31.
In states 20 and 21, the variable used to keep track of the amount of
squares spawned is swnstate4. However, states 28, 29, 30, and 31 all end
up using swnstate4, and at the end of states 28 and 29, swnstate4 will
be 7, and at the end of states 30 and 31, swnstate4 will be 3. This
means if we go to states 20 and 21 after coming from states 28 and 29,
we will only get 1 square, and if we go to states 20 and 21 after coming
from states 30 and 31, we will only get 4 squares.
This can be clearly filed under a failure to reset appropriate state.
What's the solution here? Just reset swnstate4 in state 3, so there will
be 7 squares, as intended. This also fixes the bug for state 22 as well,
which is affected in the same manner.
This fixes an oversight that could lead to confusion by the player.
showtargets is the variable that shows all unexplored teleporters on the
map as a question mark, so players know where to head to to make
progress. However, it previously was not directly saved to the main game
file. Instead, it would be set to true if flag 12 was turned on in the
save file.
How well does flag 12 correlate with showtargets?
Well, the script that turns on showtargets (bigopenworld and
bigopenworldskip) doesn't turn it on. Neither does completing Space
Station 1.
This flag is only turned on when the player activates Violet's activity
zone for the first time.
Therefore, it's entirely possible that a new player could complete Space
Station 1, then save their game, and come back to resume playing later.
When they do come back, the question marks that Violet told them about
won't show up on the minimap, and they'll be confused. They may not know
where to go.
And it is completely unintuitive for them to know that in order to get
the question marks to show up again, they have to not only talk to
Violet, but then save the game again, and reload the save. Especially
since the question marks only show up after you reload the save, and not
when you talk to Violet (because flag 12 is only a proxy for
showtargets, not the actual variable itself).
So what's the solution? Just save showtargets to the save file directly.
If you have invincibility enabled, the tower camera behavior is
inconsistent.
In ascending towers, you can "push" the camera upwards; however you
cannot push it downwards; at least it stays still when it comes up to
you if you stay still. In descending towers, the camera moves quicker
when you're at the bottom of the screen, but it's slower than your
falling speed and quickly loses sight of you; the camera can be pushed
upwards; unfortunately it also does a "bumping" motion if you're
standing still when the camera reaches you, which gets real annoying and
isn't particularly pleasant to look at.
There are two problems, so this does two fixes:
1. Pushing the camera now applies the appropriate counter-offset
depending on the direction of the tower. You can now push the camera
downwards in ascending towers.
2. To fix the "bumping" when the camera reaches you if you stand still,
there are now a 8-pixel-high "gray areas" at the top and bottom of
the screen where the camera simply won't move if you're in them.
Doing these camera offsets instead of simply canceling the movement if
the player is offscreen is a bit ugly... but it works for now.
This is a lot of copy-pasted code, but a little bit of copy-pasting
never hurt anyone...
The keybind to interact with activity zones and teleporters is now
separate from the keybind to open the map, or return to the editor from
in-editor playtesting, or restart a time trial. The keybind is now E,
and the default controller bind is X. No controller button prompts, but
the game didn't have controller button prompts anyways, so whatever.
Doing this now because if people's muscle memory are going to be broken
by not being able to spam the map keybind anymore, at least we can help
a bit by changing the keybind so they can keep spamming it - their
muscle memory is going to be broken anyways.
This option has to be enabled by going to the speedrunner menu options
and selecting "interact button". It is disabled by default.
All prompt text needs to be string-interpolated every time they are
drawn, because it is possible for people to change which interact button
they use in the middle of gameplay, via the in-game options.
Closes#736.
Colors in over-30-FPS mode shouldn't be updating every deltaframe;
mostly to ensure determinism between switching 30-mode and over-30 mode.
I'm going to overhaul RNG in 2.4 anyway, but right now I'm going to fix
this because I missed it.
The RNG of each special text box is stored in a temporary variable on
the text box itself, and only updated if the color uses it (hence the
big if-statement). Lots of code duplication, but this is acceptable for
now.
After the dimension destabilizes, the song that plays is Positive Force.
Which has already been played twice in the game at that point (first in
Tower, then in the Gravitron). Since Piercing the Sky is unused, why not
play a song that the player hasn't heard before? It would also be
musically fitting for the scenario.
The song gets played in two places - one for if you have cutscenes
enabled, and one for if you don't - so we just need to change both of
them.
I asked Terry in Discord DMs if he wanted this change and he approved of
it.
If you have completed No Death Mode, and entered the Master of the
Universe trophy room in the Secret Lab in over-30-FPS mode, it would
appear to start at one position before quickly zipping to another during
the deltaframes.
This is because it updates its position after the initial assignments of
lerpoldxp/lerpoldyp in entityclass::createentity().
Other entities do this too, and what's been done for them is to
copy-paste the lerpoldxp/lerpoldyp updates alongside the xp/yp updates.
However, instead of single-case patching this deltaframe glitch, I've
opted to fix ALL cases by simply moving the lerpoldxp/lerpoldyp
assignments to the end of the function, guaranteeing that all entities
that update their position after the initial assignment in the function
will not have any deltaframe glitches.
Of course, there's still the duplicate lerpoldxp/lerpoldyp updates in
entityclass::updateentities()... I'm not sure what to do about those.
If you had Flip Mode enabled when exiting from in-game options, the game
would flash the in-game options menu as flipped for 1 frame before
returning to the pause menu.
To fix this, just defer the Flip Mode variable assignment to be done at
the end of the frame.
This is a small quality-of-life fix in the same vein as allowing the
player to press Esc in the teleporter menu (which they weren't able to
do in 2.2, either).
This fixes the finalstretch tile shifting persisting if you return to
the main dimension and final_colormode isn't reset properly.
It's possible to do so in the main game by using a teleporter in
finalmode while having the Intermission 1 or 2 companion active.
For custom levels, level makers can make a setup that automatically
turns on finalstretch, goes to finalmode, and then returns to the main
dimension. The only thing being... as a level maker myself, this tile
shifting REALLY doesn't seem useful (and no one has ever used it because
the setup to do so hadn't really been found or documented until this
year). For one, the exact shift is randomized every time (there's an
fRandom() call to cycle the colors). For two, it goes away after the
player saves and reloads the level. And for three, it doesn't animate
like it does in finalmode (this is the biggest reason IMO).
Nevertheless, I've decided to keep support for this in custom levels, in
case someone in the future does want to use it and is okay with the
limitations.
There's a bit of inconsistency with how long each color lasts for during
final stretch. Initially, each color lasts for 40 frames, but when you
enter either of the minitowers, the color switches to lasting for 15
frames only. This is because a final_colorframe of 1 makes it go for 40
frames, but a final_colorframe of 2 makes it go for 15 frames - and
final_colorframe gets set to 2 whenever you enter a minitower.
This seems like an oversight because (1) final_colorframe doesn't affect
anything inside the minitower, (2) final_colorframe doesn't get saved to
the save file and always gets set to 1 if your save file has
finalstretch set to true, so saving and reloading will set the colors
back to 40 frames each, and (3) final_colorframe doesn't get set back to
1 when leaving the minitowers.
When you enter the Super Gravitron, you have to wait until the Super
Gravitron actually starts before being able to press Enter to return to
the Secret Lab. This is annoying if you just want to get back to the
Secret Lab. So, I've made it so the press-Enter-to-return functionality
is enabled from the moment that the Super Gravitron starts.
It turns out, despite the game attempting to prevent you from using
invincibility or slowdown in the Super Gravitron by simply preventing
you from entering the Secret Lab from the menu, it's still possible to
enter the Super Gravitron with it anyways. Just have invincibility or
slowdown (or both!) enabled, enter the game normally, and talk to
Victoria when you have 20 trinkets, to start the epilogue cutscene.
Yeah, that's a pretty big gaping hole right there...
It's also possible to do a trick that speedrunners use called
telejumping to the Secret Lab to bypass the invincibility/slowdown
check, too.
So rather than single-case patch both of these, I'm going to fix it as
generally as possible, by moving the invincibility/slowdown check to the
gamestate that starts the Super Gravitron, gamestate 9. If you have
invincibility/slowdown enabled, you immediately get sent back to the
Secret Lab. However, this check is ignored in custom levels, because
custom levels may want to use the Super Gravitron and let players have
invincibility/slowdown while doing so (and there are in fact custom
levels out in the wild that use the Super Gravitron; it was like one of
the first things done when people discovered internal scripting).
No message pops up when the game sends you back to the Secret Lab, but
no message popped up when the Secret Lab menu option was disabled
previously in the first place, so I haven't made anything WORSE, per se.
A nice effect of this is that you can have invincibility/slowdown
enabled and still be able to go to the Secret Lab from the menu. This is
useful if you just want to check your trophies and leave, without having
to go out of your way to disable invincibility/slowdown just to go
inside.
This factors out the slowdown and invincibility conditionals to a
function. This means less copy-pasted code, and it also conveys intent
(that we don't want to allow competitive options if we have either of
these cheats enabled).
This function isn't implemented in the header because then we would have
to include Map.h for map.invincibility, and transitive includes are
evil. Although, map.invincibility ought to be on Game instead (it was
only mapclass due to 2.2-and-previous argument passing), but that's a
bunch of variable reshuffling that can be done later.
They are now factored out to an inline function named incompetitive().
This is so their usage can be changed without having to change each
individual one in every place. This also clarifies the intent of using
these conditionals (they are for when we're in a "competitive" mode).
Tower backgrounds have a bypos and bscroll. bypos is just the y-position
of the background, and bscroll is the amount of pixels to scroll the
background by on each frame, which is used to scroll it (if it's not
being redrawn) and for linear interpolation.
For the tower background (and not the title background), bypos is
map.ypos / 2, and bscroll is (map.ypos - map.oldypos) / 2. However,
usually bscroll gets assigned at the same time bypos is incremented or
decremented, so you never see that calculation explicitly - except in
the previous commit, where I worked out the calculation because the
change in y-position isn't a known constant.
Having to do all these calculations every time introduces the
possibility of errors where you forget to do it, or you do it wrongly.
But that's not even the worst; you could cause a linear interpolation
glitch if you decide to overwrite bscroll without taking into account
map.oldypos and map.ypos.
So that's why I'm adding a function that automatically updates the tower
background, using the values of map.oldypos and map.ypos, that is used
every time map.ypos is assigned. That way, we have to write less code,
you can be sure that there's no place where we forget to do the
calculations (or at least it will be glaringly obvious) or we do it
wrongly, and it plays nicely with linear interpolation. This also
replaces every instance where the manual calculations are done with the
new function.
If you have invincibility enabled and push the camera, the background
would smear. This is because the game doesn't calculate the proper
bscroll and bypos of the tower background, and also doesn't end up
redrawing it.
We do both these things now, so this is fixed.
These places didn't assign map.oldypos when they assigned map.ypos. This
could have only resulted in visual glitches, but it's good to be
consistent and proactively fix these.
This fixes issues where they would be silent for 1 frame due to frame
ordering, resulting in a weird-sounding beginning of these tracks due to
a lack of attack (in the musical sense).
This is similar to the issue where tracks fading in would suddenly be
loud for 1 frame, again due to frame ordering.
This fixes issues with music playing, only for it to fade out
afterwards. This happened if tracks 0 or 7 were played after fading out,
because playing other tracks reset the fade booleans (by calling a
fade-in), but not tracks 0 or 7.
The previous fade system used only one variable, the amount of volume to
fade per frame. However, this variable was an integer, meaning any
decimal portion would be truncated, and would lead to a longer fade
duration than intended.
The fade per volume is calculated by doing MIX_MAX_VOLUME / (fade_ms /
game.get_timestep()). MIX_MAX_VOLUME is 128, and game.get_timestep() is
usually 34, so a 3000 millisecond fade would be calculated as 128 /
(3000 / 34). 3000 / 34 is 88.235..., but that gets truncated to 88, and
then 128 / 88 becomes 1.454545..., which then gets truncated to 1. This
essentially means 1 is added to or subtracted from the volume every
frame, and given that the max volume is 128, this means that the fade
lasts for 128 frames. Now, instead of the fade duration lasting 3
seconds, the fade now lasts for 128 frames, which is 128 * 34 / 1000 =
4.352 seconds long.
This could be fixed using floats, but when you introduce floats, you now
have 1.9999998 problems. For instance, I'm concerned about
floating-point determinism issues.
What I've done instead is switch the system to use four different
variables instead: the start volume, the end volume, the total duration,
and the duration completed so far (called the "step"). For every frame,
the game interpolates which value should be used based on the step, the
total duration, and the start and end volumes, and then adds the
timestep to the step. This way, fades will be correctly timed, and we
don't have potential determinism issues.
Doing this also fixes inaccuracies with the game timestep changing
during the fade, since the timestep is only used in the calculation
once at the beginning in the previous system.
To exclude gravitron squares, the game excluded all entities whose
`size` was 12 or higher. The `size` of the player when they transform
into VVVVVV-Man is 13.
We have already inadvertently fixed VVVVVV-Man not warping vertically in
2.2. This was done with the previous room transition/warping code
refactors; the gravitron square conditionals were simply excluded from
the vertical warp code, because there's no situation where there would
ever be a gravitron square outside the screen vertically.
As with making rescuable crewmates warpable, I have yet to ever see
people use VVVVVV-Man in a custom level. It's not like they would want
to use it anyway; VVVVVV-Man is really, really buggy. And it's probably
better to make it less buggy, starting with this commit.
That being said, VVVVVV-Man's collision when warping horizontally is
really janky, so I still wouldn't use it.
The game excluded every entity whose `type` was 50 or higher. The `type`
of rescuable crewmates is 55.
Could some levels be broken by this behavior? Unlikely; without warping,
the crewmates would end up falling out of the room and would become
unrescuable. So this is more likely to fix than to break.
But more importantly, *no one knows that rescuable crewmates don't
warp*. If anyone would know, it would be me, because I've been in the
custom levels community for over 7 years - and yet, during that time, I
have not seen anyone run into this corner case. If they did, I would
remember! This implies that people simply have never thought about
putting rescuable crewmates in places where they would warp - or they
have, ran into this issue, and worked around it.
With those two reasons, I'm comfortable fixing this inconsistency.
This saves one indentation level. I also fixed the comments a bit
(multiline instead of single-line, "gravitron squares" instead of "SWN
enemies", also commented the player exclusion from horizontal wrapping
in vertically-wrapping rooms).
This fixes a bug where using the fullscreen toggle keybind (Alt+Enter,
Alt+F, or F11) wouldn't update the color of the "resize to nearest" menu
option. The color doesn't functionally change anything - the option
still won't work, and will still have the message telling you that you
need to be in windowed mode when you move your menu selection to it -
but it's an easy inconsistency to fix; just move the menu recreation in
to Screen::toggleFullScreen() itself.
The game dereferences graphics.screenbuffer without checking it first...
it's unlikely to happen, but the least we can to do be safe is to add a
check and assert here.
If there were two scripts with the same name, removing one of them would
only remove the other script from the script name list, and not also
remove the contents of said script - leading to a desync in state, which
is probably bad.
Fixing this isn't as simple as removing the break statement - I either
also have to decrement the loop variable when removing the script, or
iterate backwards. I chose to iterate backwards here because it
relocates less memory than iterating forwards.
No need to use it when good ol' loops work just fine.
Iterating backwards is correct here, in case there happen to be more
than one of the item in the vectors, and also to minimize the amount of
memory that needs to be relocated.
This is a simple change - we draw minimap.png, instead of the generated
custom map, if it is a per-level mounted custom asset.
Custom levels have already been able to utilize minimap.png, but it was
limited - they could do gamemode(teleporter) in a script, and that would
show their customized minimap.png, but it's not like the player could
look at it during gameplay.
I would have done this earlier if I had figured out how to check if a
specific asset was mounted or not.
Previously, if the game couldn't set the write dir to the base
directory, or couldn't make the base directory, or couldn't calculate
the base directory, it would probably dereference NULL or read from
uninitialized memory or murder your family or something. But now, I've
eliminated the potential Undefined Behavior from the code dealing with
the base path.
Previously, this function had a bug due to failing to account for array
decay. My solution was to just repeat the MAX_PATH again. But in
hindsight I realize that's bad because it hardcodes it, and introduces
the opportunity for an error where we update the size of the original
path but not the size in the function.
So instead, just pass the size through to the function.
I don't want to add too many asserts, because sometimes it's okay if a
file is missing (mmmmmm.vvv). But currently, the game basically expects
all images and sound effects to be present. That might change in the
future, but for now, these asserts are okay.
FILESYSTEM_loadFileToMemory() dereferenced pointers without checking if
they were valid... I don't know of any cases where they could have been
NULL, but better safe than sorry.
So, the codebase was kind of undecided about who is responsible for
initializing the parameters passed to FILESYSTEM_loadFileToMemory() - is
it the caller? Is it FILESYSTEM_loadFileToMemory()? Sometimes callers
would initialize one variable but not the other, and it was always a
toss-up whether or not FILESYSTEM_loadFileToMemory() would end up
initializing everything in the end.
All of this is to say that the game dereferences an uninitialized
pointer if it can't load a sound effect. Which is bad. Now, I could
either fix that single case, or fix every case. Judging by the title of
this commit, you can infer that I decided to fix every case - fixing
every case means not just all cases that currently exist (which, as far
as I know, is only the sound effect one), but all cases that could exist
in the future.
So, FILESYSTEM_loadFileToMemory() is now guaranteed to initialize its
parameters even if the file fails to be loaded. This is better than
passing the responsibility to the caller anyway, because if the caller
initialized it, then that would be wasted work if the file succeeds
anyway because FILESYSTEM_loadFileToMemory() will overwrite it, and if
the file fails to load, well that's when the variables get initialized
anyway.
My next commit will involve using goto to jump to the end of a function
to initialize the variables to NULL, but that results in a compiler
error if we have initializations in the middle of the function. We might
as well put all declarations at the top of each block anyway, to help
the move to C, so I'm doing this now.
Since the length variable in the STDIN block now overshadows the length
variable in the outer block, I've renamed the length variable in the
block to stdin_length.
These casts are sprinkled all throughout the graphics code when creating
and initializing an SDL_Rect on the same line. Unfortunately, most of
these are unnecessary, and at worst are wasteful because they result in
narrowing a 4-byte integer into a 2-byte one when they don't need to
(SDL_Rects are made up of 4-byte integers).
Now, removing them reveals why they were placed there in the first place
- a warning is raised (-Wnarrowing) that implicit narrowing conversions
are prohibited in initializer lists in C++11. (Notably, if the
conversion wasn't narrowing, or implicit, or done in an initializer
list, it would be fine. This is a really specific prohibition that
doesn't apply if any of its sub-cases are true.)
We don't use C++11, but this warning can be easily vanquished by a
simple explicit cast to int (similar to the error of implicitly
converting void* to any other pointer in C++, which works just fine in
C), and we only need to do it when the warning is raised (not every
single time we make an SDL_Rect), so there we go.
This fixes a bug where after loading in to the level editor, pressing
Esc and then switching your option to something other than the first
option, then pressing Esc again to close the menu, then pressing Esc
once more would not keep your menu option.
This is because the code that checks if Menu::ed_settings is already in
the stack doesn't account for if ed_settings is the current menu - the
current menu doesn't get put in to the stack.
In hindsight, maybe I could have designed the new menu system better so
the current menu IS on the stack, and/or should have used a
statically-allocated linked list for each menu name for the stack frames
(instead of an std::vector) and asserted if a menu that already existed
in the stack was created instead... that'll have to be done later,
though.
Pressing Esc to cancel the confirm quit menu didn't play the squeak, in
contrast to pressing ACTION to cancel it, so now it does; pressing Esc
to close the pause menu or pressing ACTION will also now play the
Viridian squeak too.
vx/vy mean x-velocity and y-velocity... except here, where it seems like
they're used as extra parameters that do different things depending on
the entity. But it seems like at one point they were actually meant to
be the speed of the entity (this is the case for the unused decorative
particle entities), and then just never got renamed when they weren't.
The custom levels community named these two parameters meta1 and meta2
in the reference list of entities for the createentity() script command,
so that's what I'm naming them here. This will avoid confusion (I know
that some people reading this function have genuinely mistaken the vx/vy
for actually meaning x-velocity and y-velocity, simply because they were
named that way).
I have spelled out each overloaded version instead, and only the
overloads that are actually used - which just happens to be everything
except the 8-argument one. I don't want to deal with callers right now
(there are too many of them), so I'm not going to change the names that
the callers use, nor do I want to change the amount of arguments any
existing callers use right now - but we will have to deal with them in
one way or another when we move to C.
The script command createentity() is always an int. But not only that,
every time createentity() is used, its arguments are always treated like
ints. Always. I knew that vx/vy were floats because of the int casts in
the function, but I didn't even realize that xp/yp were floats, too,
until I checked just now! That's how much they're treated like ints.
All int casts in createentity() have also been removed, due to being
unnecessary (either because of us suppressing MSVC implicit conversion
warnings, or because there are now no longer any conversions happening).
This boolean is assigned, and it is checked... but it's never assigned
to true, thus making it useless. I also checked 2.2 source and the same
thing happens there; to prevent any confusion, I'm removing this.
So... I did see that map.ypos was a float when I added over-30-FPS mode,
because map.oldypos wasn't there before... I'm guessing that I kind of
just ignored it at the time. But, c'mon, map.ypos and map.oldypos are
always treated as ints, so there's literally no reason for them to be
actually floats in reality. I didn't even know they were anything other
than ints until I checked Map.h.
This is quite simple - whenever the user uses their keyboard or
controller, we hide the mouse cursor. Whenever they move the mouse, we
show it again. This makes it so the cursor gets out of the way when they
play the game, but reappears when they need it.
There is also a timeout, to prevent strobing if the user decides to use
the keyboard/controller and mouse at the same time. There is no timeout
from hiding the mouse cursor, but there is a timeout from showing the
mouse cursor - this because it's okay if the mouse lingers for a few
frames when you start using the keyboard, but really annoying if the
mouse doesn't instantly appear when you move it.
The config option has been removed. I'm going to implement something
that automatically shows and hides the mouse cursor whenever
appropriate, which is better than a config option.
These are two C++ features that we don't need, don't use, and will never
use in the future. Apparently the best way of doing this in CMake is to
fiddle with the CXX_FLAGS using regex.
Now this is one less flag I need to supply myself when I invoke CMake...
This variable is not defined anywhere and never has been since the
source code release (which is when this CMake file was first created).
To make things clearer, I'm cleaning this variable up.
A function like add_definitions() adds definitions to ALL targets, not
just VVVVVV. This kind of namespace pollution is messy, and could result
in bugs if you pollute with the right kind of pollutant.
So instead of using add_definitions(), use target_compile_definitions().
And instead of using include_directories(), use
target_include_directories().
All the C third-party dependencies are C90, and all the C files we have
are also C90 (well, almost, but that's easily sorted). So we have
basically no reason to not go with C90 here.
The only wrinkle is, turning C extensions off for physfs-static results
in linker errors because PhysFS implicitly uses alloca() without
including it properly (on Linux). I am not the only one who has ran into
this - see https://icculus.org/pipermail/physfs/2020-April/001293.html -
and it's a bug with PhysFS. The workaround I've gone with is to enable C
extensions. (There might also be some funkiness with PhysFS's use of the
`inline` keyword, so enabling extensions will paper over that as well.)
So there were actually only two instances of C99-style end-of-line
comments in C files - and technically one of them was just a C file
including MakeAndPlay.h.
It seems like CMake 3.1.3 introduced the C/C++ standard properties,
while the minimum version of this CMake file is 2.8.12. So we do what
FAudio does, which is print a warning if the CMake version is too old
and otherwise use it if we have the feature.
They're the same thing, but using option() better conveys intent.
However this can't be done for anything that isn't a bool, which the
CUSTOM_LEVEL_SUPPORT option is not (it's a tri-state string).
These were introduced in 098fb77611 - did
Leo not know that they were already there at the top of the file? This
does the same thing, except it only sets it for VVVVVV instead of
everything (so this wouldn't set it for the third-party dependencies).
If a track was restarted after it faded out, then it wouldn't play. This
is because currentsong wasn't set to -1 after fading out, and that is
because the fade out calls pause() instead of haltdasmusik() when it
finishes.
Unlike f196fcd896, this fixes the time
trial music while keeping it to the same behavior as 2.2, and fixes
every single possible case that this music bug could have happened.
This reverts only a part of f196fcd896 -
as the original commit author did not do their changes atomically, they
also squashed in a de-duplication within the same commit. So I'm only
reverting the part of the commit that wasn't the de-duplication, which
is simply the changes to the music.fadeout() calls.
This is being (partially) reverted for several reasons:
1. It's not the correct behavior. What this does instead is persist the
track through after you restart the time trial, instead of fading it
out, then restarting it again. This is in contrast to behavior in
2.2, and I see no reason to not keep the same behavior.
2. It's a single-case patch. The time trials are not the only time in
the game a music track could fade out and then be restarted with the
same track - custom levels could do the same thing too. Instead of
fixing only one case, we should strive to fix EVERY case.
The original commit author (trelbutate) also didn't write anything in
the commit description of f196fcd896. What
you should write in the commit description is things like rationale,
analysis, and other good information that would be useful to anyone
looking at your commit to understand why you did what you did. Having no
commit description leaves readers in the dark as to why you did what you
did.
Thus, I don't know why trelbutate went with this solution, or if they
knew that it was only a single-case patching, or if they knew that it
wasn't 2.2 behavior.
By not writing the commit description, they miss a chance for
reflection; speaking from personal experience, I myself have gone back
and improved my commits countless times because I wrote commit
descriptions for every single one of them, and sometimes whenever I
write them, I think to myself "hang on a minute, that doesn't sound
quite right" and end up finding improvements.
If trelbutate wrote a commit description, they might have realized that
it wasn't 2.2 behavior, and gone back and fixed up their commit to be
correct. As it stands, though, they didn't have to think about it in the
first place because they never bothered to write a commit description.
edteleportent is a global variable that gets assigned whenever the
player collides with a warp token, and gets read from later down the
line in gamelogic(). While I don't know of any way to cause anything bad
with this (and I did try), storing a temporary indexing variable like
this is only bound to be a liability in the future - so we might as well
prevent badness now by adding a bounds check here.
This fixes a bug where quitting to the menu from command-line
playtesting with -playassets specified would always use those assets
when loading back in to any custom level. This also fixes loading in to
a custom level quicksave always using the command-line playtesting
arguments instead of using the actual quicksave.
In a vertically-warping room, the 'height' of the room becomes 232
pixels, regardless of if you have a room name or not. So the remaining 8
rows of pixels at the bottom of the screen corresponds with the first 8
rows of pixels at the top of the screen, and entities in the bottom 8
rows of pixels get teleported to the top of the screen.
The screen wrapping drawing code doesn't draw entities in the top 8 rows
of pixels at the bottom, leading to a discontinuous effect where it
looks like vertically-warping entities don't neatly change from the
bottom to the top or vice versa - this is especially noticeable with
enemies. To fix this, just increase the threshold for drawing top
entities at the bottom of the screen by 8 pixels.
When an entity vertically warps, it teleports upwards or downwards by
232 pixels. However, the graphics code draws them with an offset of 230
pixels. This is off by 2 pixels, but it's enough to make a
downwards-moving enemy look like it suddenly collides with the bottom of
the screen (in a room without a room name) before it warps, especially
if you go frame-by-frame.
It seems like for whatever reason that the frames portion of save files
is never read from, and always zeroed. Well, technically they get parsed
but the result is immediately discarded afterwards.
I see no reason to do this, so I'm removing these zeroes.
This fixes being able to make music fully fade in (or out) by unfocusing
the game, or making the fade bars fully fade in (or out) by unfocusing
the game, or racking up the timer while the game is unfocused.
In 2.2 and previous, the game would call resetgameclock() every frame
for the last 30 frames of the time trial countdown in order to make sure
it gets reset. This was in a render function, and didn't get brought out
in 2.3, so 2.3 resets the game clock *while rendering*, which is kinda
bad and is an oversight on my part for not noticing.
Instead of doing that, just add a conditional to the timer so that it
won't tick during the time trial countdown. This fixes#699 even further
by making it so the time trial par can't even be lost during the
countdown, because the timer won't tick up - so you can never get a sad
squeak to play by pausing the game or unfocus-pausing it during the
countdown.
For some reason, resetgameclock() is only ever used in gamerender(), and
everywhere else just zeroes the clock manually. This is weird to me, so
I've made it so everywhere that zeroes the clock uses the
resetgameclock() function to do so.
Otherwise, if the timer ticked up past the par (via using the unfocus
pause or pause menu), it would result in the sad squeak being played
every frame because the game would constantly be setting
timetrialparlost, then moving to the code block below, assuming that
since timetrialparlost that we haven't lost the par already, and playing
the squeak.
timetrialparlost gets reset in hardreset() and startgamemode() anyways,
so there's no need to be constantly resetting this variable.
Fixes#699.
It turns out this entire chunk of code is simply unneeded (and is
actively harmful) since when we're done with the time trial,
quittomenu() gets called, and that removes the previous stack frame
anyway.
I'm guessing that I added this code, then added quittomenu(), then
didn't consider how this code and quittomenu() would mix. But anyways,
this bug is fixed.
Fixes#714.
This seems to be a comment left by Ethan that he never got around to. So
I did it for him.
What I've done is made it so FileSystemUtils.cpp knows what a binary
blob is, and moved the binary blob loading code directly to
FileSystemUtils.cpp. To do this, I removed the private access modifier
from binaryBlob - I don't think we'll need it, and anyways when we move
to C we can't use it.
Along the way, I also cleaned up the style of the function a bit - the
null termination offset is no longer hardcoded, and the function no
longer mixes code and declarations together in the same block.
I also noticed that when printing all the filenames at the end, a single
invalid header would stop the whole loop instead of just being skipped
over... this seems to be a bug to me, so I've made it so invalid headers
just get skipped over instead of stopping the whole loop.
In FileSystemUtils.h, I used a forward declaration. In hindsight,
incomplete forward declarations should basically always be done in
header files if possible, otherwise this introduces the possibility of
transitive includes - if a file includes this header and it does a full
include, the file is silently able to use the full header, whereas if
it's a forward declaration, then the moment the file tries to use the
full header it fails, and then it's forced to include the full header
for itself. But uh, that's a code cleanup for later.
While fixing all the other music bugs, I discovered that starting
playtesting in the editor wouldn't play the level music.
The problem is that the editor playtesting start code calls
music.fadeout() before calling music.play(). This queues up the track
from the music.play() call. After that, what should happen is that
processmusic() processes the fade, the fade is then finished, and then
after that it sees that the music is halted so it can play the queued
track.
Instead what happens is that the function first attempts to play the
music before the fade is processed and finished, so play() will re-queue
the music again, but the queue gets cleared right after that (this is a
subtle bit of behavior - it means if the game fails to play a queued
track due to it fading, it's not going to re-queue it again and end up
in some sort of infinite loop).
This is a frame ordering issue - the function is tripping over itself
when it shouldn't be. To fix it, just put the queue processing code
after the fade processing code.
This fixes the 2.2-and-below music blocking workaround not working in
2.3.
The issue was that when the music got halted by the script, the fade
volume would still be processing, silently being decremented in the
background. So the script playing the track afterwards would make the
game queue it (as it was called during the fade), but then the music is
halted so the game would attempt to play it, but the fade is STILL
happening so it wouldn't actually play it and would attempt to queue the
track again.
However, that queue gets discarded immediately afterwards because the
music.play() call happened inside the code responsible for playing the
queued music, and that code unconditionally clears the queue variables
immediately after calling play(). So that's good to know - if the game
queues a song, but fails to play it because of a fade, it's not going to
immediately re-queue it and potentially get stuck in a loop of
infinitely queueing the same song over and over again each frame.
Anyways, the source of the problem is not resetting the fade booleans
when halting music, so I've reset them.
Fixes#701.
The problem here is that even though we start playing the music when the
volume is set to zero, mixer's state doesn't have volume zero, so
whatever it plays next will be the very first quanta of the track but at
the previous volume (in this case, the maximum volume). To fix this,
just update mixer when we update the volume here - it's okay to not
account for user volume because it ends up being zero anyway.
Fixes#710.
This fixes a bug where fading music in but not going through the
music.play() path wouldn't start the fade volume from zero. If this
happened, then the previous volume would persist, and if the previous
volume was the max volume, then that essentially canceled out the
fade-in and prevented it from happening at all. But now all paths to
fadeMusicVolumeIn() set the volume to zero first, instead of only the
caller of music.play().
When you pick up a trinket in the wild, the music gets silenced, so it
silently plays in the background until you advance the trinket text.
However, foundtrinket (used when Victoria or Vitellary give you a
trinket) is inconsistent with this, and halts the music instead of
silencing it.
This was probably due to the musicfadein script command not being
implemented, so Terry or Simon had to simply make do and halt the music
instead. But musicfadein is implemented and is being used in the trinket
cutscenes, so this is another inconsistency that I will fix.
When you pick up a trinket in the wild, the music will fade back in
afterwards. However, the special trinket cutscenes (where Victoria or
Vitellary will directly give you a trinket) are inconsistent with this,
and restart the music instead of fading it back in.
Looking at the scripts themselves, it immediately becomes obvious the
reason for this inconsistency - 2.2 and previous didn't implement the
musicfadein command, so it couldn't be used, and Terry or Simon simply
had to make do with simply restarting the music. However, 2.3 implements
musicfadein, so we can simply swap it out and remove the
trinketscriptmusic command.
This is 2.2 behavior, which I forgot to keep. Otherwise, if music has
halted and you try to play the same track, it simply won't work, because
the current song is the same as the song you're trying to play. This is
what happened with the trinket scripts - the game halted music, then
tried to play the same track.
Fixes#712.
It's not really used because CreateDirectory doesn't support setting
chmod values, but it does clarify intent of the argument.
Co-authored-by: Ethan Lee <flibitijibibo@gmail.com>
In #52 I fixed VVVVVV not being able to handle filepaths with non-ASCII
characters on Windows. 2f0a0bce4c and
aa5c2d9dc2 reintroduce this problem,
however, by reverting the definition of mkdir to how it was before the
fix and using the non-Unicode version of CreateDirectory. And I can
confirm that VVVVVV indeed doesn't make its folder anymore with a
Windows username of "тест". This commit fixes that issue.
This adds music and volume sliders to the audio options. To use the
sliders, you navigate to the given option, then press ACTION, and your
selection will be transferred to the slider. Pressing left or right will
move the slider accordingly. Then you can press ACTION to confirm the
volume is what you want and deselect it, or you can press Esc to cancel
the volume change, and it will revert to the previous volume; both
actions will write your settings to disk.
Most of this commit is just adding infrastructure to support having
sliders in menus (without copy-pasting code), which is a totally
completely new user interface that has never been used before in this
game. If we're going to be adding something new, I want to make sure
that it at least is done the RIGHT way.
Closes#706.
This adds <musicvolume> and <soundvolume> tags to unlock.vvv and
settings.vvv, so users' volume preferences will be persistent across
game sessions. This does not add the user interface to change them from
in-game; the next commit will do that.
This function is simple - it takes a given buffer and its size, fills it
with a certain character, and null-terminates it. It's meant to be used
with freshly-created buffers, so we don't copy-paste code.
Pressing return in gameplay options would send you back to the pause
menu instead of the general options menu, and pressing return in graphic
options would send you back to the pause menu instead of the general
options menu, too. Additionally, pressing Esc in graphic options would
also send you back to the pause menu instead of the general options
menu.
Like I said before, the menu system is still a bit hardcoded in some
places, and these happened because Terry forgot to update them when he
changed the menus around.
Fixes#711.
The in-game menu code is better than it was in 2.2 but still pretty
hardcoded, so to fix this just change each individual case around. This
bug happened because the "options" button was in the place where "quit
to menu" was previously, but Terry forgot to update it when changing all
the options around.
PhysFS requires a write dir to create a directory, so the first PHYSFS_mkdir
never could have worked. Because of that we need to go back to the old mkdir,
and since we're bringing that back we can reuse it for saves/levels, because we
know it works and we don't have to worry about middlewares ruining anything.
When a text box in the script system (not the gamestate system) is
displayed onscreen and "- Press ACTION to advance text -" is up, the
game sets pausescript to true, so the script system won't blare past the
text box and keep executing. Then it also sets advancetext to true.
Crucially, these two variables are different, so if you have pausescript
true but advancetext false, then what happens?
Well, you get softlocked. There's no way to continue the script.
How is this possible? Well, you can teleport to the (0,0) teleporter
(the teleporter in the very top-left of the map) and regain control
during the teleporter animation. To do that, in 2.2 and below, you have
to press R at the same time you press Enter on the teleporter, or in 2.3
you can simply press R during the cutscene. Then once you teleport to
the room, it's really precise and a bit difficult (especially if
Viridian is invisible), but you can quickly walk over to the terminal in
that room and press Enter on it.
Then what will happen is the terminal script will run, but the
teleporter gamestate sequence will finish and turn advancetext off in
the middle of it. And then you're softlocked.
To fix this, just add a check so if we're in gamestate 0 and there's a
script running, but we have pausescript on and advancetext off, just
turn pausescript off so the game automatically advances the script.
This softlock was reported by Tzann on the VVVVVV speedrunning Discord.
If you manage to get softlocked by being stuck in completestop, the next
thing you'll notice is that quitting to the menu or loading back in will
not reset this.
So you can actually softlock yourself in 2.3 by doing the trinket
cutscene, then quitting to the menu (because 2.3 lets you open the pause
menu during completestop). This is a bug, and should be fixed.
You can skip the "You have found a shiny trinket!" cutscene. The
conditions are that this can only be done in the main game, in the main
dimension (no Polar Dimension), the checkpoint that you last touched
must not be in the same room as the trinket, and you have to have
skipped the Comms Relay cutscene. To do the skip, you press R on the
exact frame (or previous frame, if input delay is enabled) that Viridian
touches the trinket. Then, the gamestate will be immediately set to 0
(because of the gotoroom) and the cutscene will be skipped.
Speedrunners of the main game, well, run the main game already, the
only trinket in the Polar Dimension is not one you want to do a death
warp at, and they have a habit of automatically skipping over the Comms
Relay cutscene because they press R at the beginning of the run when
Viridian teleports to Welcome Aboard, to warp back to the Ship and so
they can leave rescuing Violet for later.
So someone reported softlocking themselves by doing the trinket text
skip in 2.3. The softlock is because they're stuck in a state where
completestop is true but can't advance to a state that turns it off. How
does this happen? It's because they pressed R too late and interrupted
the gamestate sequence. In 2.2 and previous, if you're in the gamestate
sequence then you can't press R at all, but 2.3 removes this restriction
(on account of aiming to prevent softlocks). So only on the very first
frame can you death warp and interrupt the gamestate sequence before it
happens at all.
Anyways to fix this, just turn completestop off automatically if we're
in gamestate 0 and there's no script running.
This softlock was reported by Euni on the VVVVVV speedrunning Discord.
So some people reported the levels list crashing when they loaded it.
But this wasn't reproducible every time. They didn't provide any
debugging information, so I had to use my backup plan: doing a full
audit of the code path taken for loading the levels list.
And then I found this. It turns out this was because I used a
LOAD_ARRAY_RENAME() macro on an std::vector. You can't do that because
you need to use push_back() to resize a vector, so the macro will end up
indexing into nothing, causing a segfault. However, this code path would
only be taken if you have an old levelstats.vvv, from 2.2 and previous -
which explains why it wasn't 100% reproducible. But now that I know you
need an old levelstats.vvv, this bug happens 100% of the time.
Anyways, to fix this, just ditch the macro and expand it manually, while
replacing the indexing with a proper usage of push_back().
While the game does support playing levels with filenames that don't
have the .vvvvvv extension, it doesn't do it well.
Namely, those files can't be loaded or saved into the editor (because a
.vvvvvv always gets tacked on to your input when saving or loading). In
2.3, this gets worse because you can't load a level without a .vvvvvv
extension from command-line playtesting (because a .vvvvvv automatically
gets added) and you can't load per-level custom assets.
The only place where extensionless level files are supported is when
loading level metadata. But this makes it so they no longer work. This
is technically an API break, but it's easily fixed (just add the
.vvvvvv), plus there's nothing to be gained from not having an
extension, plus basically no one ever actually did this in the first
place (as far as I know, I am the only person to have ever done this,
and no one else ever has).
This fixes an issue where you would be able to mount things other than
custom assets in per-level custom asset directories and zips.
To be fair, the effects of this issue were fairly limited - about the
only thing I could do with it was to override a user-made quicksave of a
custom level with one of my own. However, since the quicksave check
happens before assets are mounted, if the user didn't have an existing
quicksave then they wouldn't be able load my quicksave. Furthermore,
mounting things like settings.vvv simply doesn't work because assets
only get mounted when the level gets loaded, but the game only reads
from settings.vvv on startup.
Still, this is an issue, and just because it only has one effect doesn't
mean we should single-case patch that one effect only. So what can we
do?
I was thinking that we should (1) mount custom assets in a dedicated
directory, and then from there (2) mount each specific asset directly -
namely, mount the graphics/ and sounds/ folders, and mount the
vvvvvvmusic.vvv and mmmmmm.vvv files. For (1), assets are now mounted at
a (non-existent) location named .vvv-mnt/assets/. However, (2) doesn't
fully work due to how PhysFS works.
What DOES work is being able to mount the graphics/ and sounds/ folders,
but only if the custom assets directory is a directory. And, you
actually have to use the real directory where those graphics/ and
sounds/ folders are located, and not the mounted directory, because
PHYSFS_mount() only accepts real directories. (In which case why bother
mounting the directory in the first place if we have to use real
directories anyway?) So already this seems like having different
directory and zip mounting paths, which I don't want...
I tried to unify the directory and zip paths and get around the real
directory limitation. So for mounting each individual asset (i.e.
graphics/, sounds/, but especially vvvvvvmusic.vvv and mmmmmm.vvv), I
tried doing PHYSFS_openRead() followed by PHYSFS_mountHandle() with that
PHYSFS_File, but this simply doesn't work, because PHYSFS_mountHandle()
will always create a PHYSFS_Io object, and pass it to a PhysFS internal
helper function named openDirectory() which will only attempt to treat
it as a directory if the PHYSFS_Io* passed is NULL. Since
PHYSFS_mountHandle() always passes a non-NULL PHYSFS_Io*,
openDirectory() will always treat it like a zip file and never as a
directory - in contrast, PHYSFS_mount() will always pass a NULL
PHYSFS_Io* to openDirectory(), so PHYSFS_mount() is the only function
that works for mounting directories.
(And even if this did work, having to keep the file open (because of the
PHYSFS_openRead()) results in the user being unable to touch the file on
Windows until it gets closed, which I also don't want.)
As for zip files, PHYSFS_mount() works just fine on them, but then we
run into the issue of accessing the individual assets inside it. As
covered above, PHYSFS_mount() only accepts real directories, so we can't
use it to access the assets inside, but then if we do the
PHYSFS_openRead() and PHYSFS_mountHandle() approach,
PHYSFS_mountHandle() will treat the assets inside as zip files instead
of just mounting them normally!
So in short, PhysFS only seems to be able to mount directories and zip
files, and not any loose individual files (like vvvvvvmusic.vvv and
mmmmmm.vvv). Furthermore, directories inside directories works, but
directories inside zip files doesn't (only zip files inside zip files
work).
It seems like our asset paths don't really work well with PhysFS's
design. Currently, graphics/, sounds/, vvvvvvmusic.vvv, and mmmmmm.vvv
all live at the root directory of the VVVVVV folder. But what would work
better is if all of those items were organized into a subfolder, for
example, a folder named assets/. So the previous assets mounting system
before this patch would just have mounted assets/ and be done with it,
and there would be no risk of mounting extraneous files that could do
bad things. However, due to our unorganized asset paths, the previous
system has to mount assets at the root of the VVVVVV folder, which
invites the possibility of those extraneous bad files being mounted.
Well, we can't change the asset paths now, that would be a pretty big
API break (maybe it should be a 2.4 thing). So what can we do?
What I've done is, after mounting the assets at .vvv-mnt/assets/, when
the game loads an asset, it checks if there's an override available
inside .vvv-mnt/assets/, and if so, the game will load that asset
instead of the regular one. This is basically reimplementing what PhysFS
SHOULD be able to do for us, but can't. This fixes the issue of being
able to mount a quicksave for a custom level inside its asset directory.
I should also note, the unorganized asset paths issue also means that
for .zip files (which contain the level file), the level file itself is
also technically mounted at .vvv-mnt/assets/. This is harmless (because
when we load a level file, we never load it as an asset) but it's still
a bit ugly. Changing the asset paths now seems more and more like a good
thing to do...
This will clarify which directory, exactly, failed to mount. I know it
gets printed earlier in the mounting process, but it can't hurt to print
it twice, just to be sure. Also this is for consistency.
Default function arguments are the devil, and it's better to be more
explicit about what you're passing into the function. Also because we
might become C-only in the future and to help faciliate that, we should
get rid of C++-isms like default function arguments now.
PHYSFS_getDirSeparator() already gets called and stored in pathSep at
the top of FILESYSTEM_init(). So clearly, two people worked on this
function and forgot that both pieces of code existed at the same time
(or it was one person independently forgetting both).
PhysFS uses platform-independent notation, so we really don't need to
care about getting the correct dir separator here. Especially since we
don't ever do so anywhere else (e.g. load/saveTiXml2Document()), either.
This is to make it clear that this is not a general-purpose mounting
function; it is a helper function for FILESYSTEM_mountAssets()
specifically for treating a directory or file as an assets directory,
and mounting assets from there.
There's no reason to handle mounting .zip files differently than
mounting a directory... we already mount .data.zip files using
FILESYSTEM_mount(), so why go through the trouble of opening a .zip
manually (which means on Windows the .zip can't be touched for the
duration of playing the custom level), making up a place to mount it at,
and then mount that made-up name, instead of just using
FILESYSTEM_mount()?
Whoever cobbled this asset mounting thing together really didn't fully
understand what they were doing.
This way, we avoid the unnecessary graphics.reloadresources() call - if
we can't mount assets, why bother reloading resources?
The return type of FILESYSTEM_mount() has been changed from void to bool
to indicate success, accomodating its callers accordingly.
I haven't been able to reproduce this old thing on any setup I have. The patch
from 2013 was originally for X11, and Wayland's fullscreen doesn't allow for
this sort of thing, so let's start scoping this down for eventual removal when
X11 is finally out of our minds forever.
So it looks like facb079b35 (PR #316) had
a few issues.
The SDL performance counter doesn't really work that well. Testing
reveals that unfocusing and focusing the game again results in
the resumemusic() script command resuming the track at the wrong time.
Even when not unfocusing the game at all, stopping a track and resuming
it resumes it at the wrong time. (Only disabling the unfocus pause fixes
this.)
Furthermore, there's also the fact that the SDL performance counter
keeps incrementing when the game is paused under GDB. So... yeah.
Instead of dealing with the SDL performance counter, I'm just going to
pause and resume the music directly (so the stopmusic() script command
just pauses the music instead). As a result, we no longer can keep
constantly calling Mix_PauseMusic() or Mix_ResumeMusic() when focused or
unfocused, so I've moved those calls to happen directly when the
relevant SDL events are received (the constant calls were originally in
VCE, and whoever added them (I'm pretty sure it was Leo) was not the
sharpest tool in the shed...).
And we are going to switch over to using our own fade system instead of
the SDL mixer fade system. In fact, we were already using our own fade
system for fadeins after collecting a trinket or a custom level
crewmate, but we were still using the mixer system for the rest. This is
an inconsistency that I am glad to correct, so we're also doing our own
fadeouts now.
There is, however, an issue with the fade system where the length it
goes for is inaccurate, because it's based on a volume-per-frame second
calculation that gets truncated. But that's an issue to fix later - at
least what I'm doing right now makes resumemusic() and musicfadein()
work better than before.
musicclass already had a resume() function for music.
These are just wrappers around the appropriate SDL_mixer functions, to
avoid direct function calls to the mixer API. So if we ever need to do
something with all callers of pausing and resuming in the future, or we
switch to a different audio backend, the work is already done for us.
Also it just looks cleaner to be calling our musicclass function instead
of doing a direct API call to the mixer.
This makes it so to reuse this code, we don't have to copy-paste it.
Additionally, I added a check for the milliseconds being 0, to avoid a
division by zero. Logically and mathematically, if the fade amount is 0
milliseconds, then that means the fade should happen instantly -
however, dividing by zero is undefined (both in math and in C/C++), so
this check needs to be added.
This is an option for speedrunners whose muscle memory is precisely
trained and used to the 1-frame input delay that existed in 2.2 and
below. It is located in Game Options -> Advanced Options, and is off by
default.
To re-add the 1-frame input delay, we simply move the key.Poll() to the
start of the frame, instead of before an input function gets ran -
undoing what #535 did.
There is a frame ordering-sensitive issue here, where toggling
game.inputdelay at the wrong time could cause double-polling. However,
we only toggle it in an input function, which regardless is always
guaranteed to be ran after key.Poll() (it either happened at the start
of the frame or just before the input function got ran), so this is not
an issue. But, in case we ever need to toggle this variable in the
future, we can just use the defer callbacks system to defer the toggle
to the end of the frame - also added by #535.
Added at the request of Habeechee on the VVVVVV speedrunning Discord
server.
This fixes being unable to use teleporters while the "- Press ACTION to
advance text -" prompt is up, which is used to perform credits warp.
In 2.2 and 2.0, this advancetext check was only in gamerender() for
rendering the "- Press ENTER to Teleport -" prompt and didn't affect any
logic. In 2.3, I moved the check (and the rest of the conditional it was
in) to gamelogic() - same as the activity zone prompt conditionals - so
if you gained control while being in a prompt zone, the prompt wouldn't
suddenly appear[1].
As a side effect, this ended up aligning rendering and logic together,
so if you couldn't see the teleporter prompt, you weren't able to
teleport - whereas in 2.2 and 2.0, you could still use the teleporter
even though the prompt wasn't up.
So by removing the advancetext check, you are now able to use the
teleporter again, AND the "- Press ENTER to Teleport -" prompt will also
show up as well.
Habeechee reported this regression on the VVVVVV speedrunning Discord
server.
[1]: f07a8d2143, PR #421
One of the solutions to the quit signal unfocus pause regression is to
add a no-op delta func to the unfocused func table. However, this
results in the game being stuck in unfocus pause forever, because when
it reaches the end of a list on a delta func, it won't reassign the
active functions - only when the end of a list is a fixed func will it
do so. A workaround is to then add a no-op fixed func afterwards, but
that's inelegant.
The solution in the end to the quit signal regression is to not bother
with adding a delta func, so the game as of right now actually never has
a delta func at the end of a list, and probably never will - but this is
one piece of technical debt I don't want to leave laying around. In case
we're ever going to put a delta function at the end of a list, I've made
it so that delta functions will now reassign the list of active funcs if
they happen to be at the end of the func list.
This fixes a regression introduced by #535 where a quit signal (e.g.
Ctrl-C) sent to the window while the game was in unfocus pause wouldn't
close the game.
One problem was that key.quitProgram would only be checked when control
flow switched back to the outer loop in main(), which would only happen
when the loop order state machine switched to a delta function. As the
unfocused func table didn't have any delta functions, this means
key.quitProgram would never be checked.
So a naïve solution to this would just be to add a no-op delta func
entry to the unfocused func table. However, we then run into a separate
issue where a delta function at the end of a func list never reassigns
the active funcs, causing the game to be stuck in the unfocus pause
forever. Active func reassignment only happens after fixed funcs. So
then a naïve solution after that would be to simply add a no-op fixed
func entry after that. And indeed, that would fix the whole issue.
However, I want to do things the right way. And this does not seem like
the right way. Even putting aside the separate last-func-being-delta
issue, it mandates that every func list needs a delta function. Which
seems quite unnecessary to me.
Another solution I considered was copy-pasting the key.quitProgram check
to the inner loops, or adding some sort of signal propagation to
the inner loops - implemented by copy-pasting checks after each loop -
so we didn't need to copy-paste key.quitProgram... but that seems really
messy, too.
So, I realized that we could throw away key.quitProgram, and simply call
VVV_exit() directly when we receive an SDL_QUIT event. This fixes the
issue, this removes an unnecessary middleman variable, and it's pretty
cleanly and simply the right thing to do.
This includes all text from the Gravitron and Super Gravitron.
This is to make the text more readable if they are placed in weird
situations - for example, in custom levels, where the background these
texts get placed on could be anything (custom level makers are crazy!).
It's just like bigprint() except it duplicates some of the calculations
because I didn't want to make a bigprintoff() function which would
duplicate even more code. I'm beginning to think these text printing
functions are completely horrible to work with...
In case they get drawn against a non-contrasting background, it's still
useful to keep them readable by outlining them. This could happen if
someone were to use the Game Complete gamestate sequence in a custom
level (or presses R during Game Complete).
Flip Mode flips all the unfocus pause screen text upside-down, to make
it read in reverse order. This looks kind of strange to me, and I don't
think it was intended. So I'm flipping the text again so it's the right
way up in Flip Mode.
During the final stretch, after Viridian turns off the Dimensional
Stability Generator, the map goes all psychedelic and changes colors
every 40 frames. Entities change their colors too, including conveyors,
moving platforms, and disappearing platforms.
But play around with the disappearing platforms for a bit and you'll
notice they seem a bit glitchy. If you run on them at the right time,
the tile they use while disappearing seems to abruptly change whenever
the color of the room changes. If there's a color change while they're
reappearing (when you die and respawn in the same room as them), they'll
have the wrong tile and look like a conveyor. And even if you've never
interacted with them at all, dying and respawning in the same room as
them will change their tile to something wrong and also look like a
conveyor.
So, what's the problem? Well, first off, the tile of every untouched
disappearing platform changing into a conveyor after you die and respawn
in the same room is caused by a block of code in gamelogic() that gets
run on each entity whenever you die. This block of code is the exact
same block of code that gets ran on a disappearing platform if it's in
the middle of disappearing.
As a quick primer, every entity in the game has a state, which is just a
number. You can view each entity's state in
entityclass::updateentities().
State 0 of disappearing platforms is doing nothing, and they start with
an onentity of 1, which means they turn to state 1 when they get
touched. State 1 moves to state 2. State 2 does some decrementing, then
moves to state 3 and sets the onentity to 4. State 3 also does nothing.
After being touched, state 4 makes the platform reappear and move to
state 5, but state 5 does the actual reappearing; state 5 then sets the
state back to 0 and onentity back to 1.
So, back to the copy-pasted block of code. The block of code was
originally intended to fast-forward disappearing platforms if they were
in the middle of disappearing, so the player respawn code would properly
respawn the disappearing platform, instead of leaving it disappeared.
What it does is keep updating the entity, while the state of the entity
is 2, until it is no longer in state 2, then sets it to state 4.
Crucially, the original block of code only ran if the disappearing
platform was in state 2. But the other block of code, which was
copy-pasted with slight modifications, runs on ALL disappearing
platforms in final stretch, regardless of if they are in state 2 or not.
Thus, all untouched platforms will be set to state 4, and state 4 will
do the animation of the platform reappearing, which is invalid given
that the platform never disappeared in the first place. So that's why
dying and respawning in the same room as some disappearing platforms
during final stretch will change their tiles to be conveyors.
It seems to me that doing anything with death is wrong, here. The root
cause is that map.changefinalcol() "resets" the tile of every
disappearing platform, which is a function that gets called on every
color change. The color change has nothing to do with dying, so why
fiddle with the death code?
Thus, I've deleted that entire block of code.
What I've done to fix the issue is to make it so the tile of
disappearing platforms aren't manually controlled. You see, unlike other
entities in the game, the tile of disappearing platforms gets manually
modified whenever it disappears or reappears. Other entities use the
tile as a base and store their tile offset in the separate walkingframe
attribute, which will be added to the tile attribute to produce the
drawframe, which is the final thing that gets rendered - but for
disappearing platforms, their tile gets directly incremented or
decremented whenever they disappear or reappear, so when
map.changefinalcol() gets ran to update the tile of every platform and
conveyor, it basically discards the tile offset that was manually added
in.
Instead, what I've done is make it so disappearing platforms now use
walkingframe, and thus their final drawframe will be their tile plus
their walkingframe. Whenever map.changefinalcol() gets called, it is now
free to modify the tile of disappearing platforms accordingly - after
all, the tile offset is now stored in walkingframe, so no weird
glitchiness can happen there.
Ethan, you forgot this other one.
I do have to rejiggle the control flow of the function a bit, so it
doesn't leak memory upon failure. (Although the SDL message box leaks
memory anyway because of X11 so... whatever.) Also, there's a NULL check
for if SDL_GetBasePath() fails now.
According to SDL documentation[1], the returned pointer needs to be
freed. A glance at the source code confirms that the function allocates,
and also Valgrind complains about it.
Also if it couldn't allocate, the game no longer segfaults (std::strings
do not check if the pointer is non-NULL for operator+=).
[1]: https://wiki.libsdl.org/SDL_GetClipboardText
Since mainmenu is only ever used in Input.cpp, I might as well make it
clearer by moving it into a static global variable in Input.cpp. (The
same applies to fadetolab/fadetomenu, but I didn't think much about
those at the time... that'll be a refactor for later.)
While I've decoupled fademode from gamemode starting, being faded out on
the title screen results in a black screen and you being unable to make
any input. So we'll need to store the current fademode in a temporary
variable when going to in-game options, then put it back when we return
to the pause menu. Yes, you can turn on glitchrunner mode during the
in-game options, and then immediately return to the pause menu to
instantly go back to the title screen; this is intended.
Due to frame ordering, putting the fademode back needs to be deferred to
the end of the frame to prevent a 1-frame flicker.
It's actually sufficient enough to do this temporary fademode storage to
fix the whole thing, but I also decided to decouple fademode and
gamemode starting just to be sure.
Assuming glitchrunner mode is off, if you open the pause menu while
fully faded-out and then go to Graphic Options or Game Options, then the
'mode' that you selected previously will kick in again and you'll be
suddenly warped back.
So if you previously started a new game in the main game (mode 0, also
the selected mode if you do this from command-line playtesting), and
then open the pause menu and go to in-game options, then you'll suddenly
go back to starting a new game again. If you had started a custom level,
doing this will warp you back to the start of the level again.
The problem is simple - when the title screen is fully faded out, it
calls startgamemode(). So the solution is simple as well - just decouple
the fademode from calling startgamemode(), and use a different variable
to know when to actually call startgamemode().
Custom levels can have warp lines. If you have a warp line and a warping
background in the same room, the warp line takes precedence over the
warp background.
However, whenever you enter a room with a warp line and warp background,
any entities on the warping edges will be drawn with screenwrapping for
one frame, even though they never wrapped at all.
This is due to frame ordering: when the warp line gets created,
obj.customwarpmode gets set to true. Then when the screen edges and
warping logic gets ran, the very first thing that gets checked is this
exact variable, and map.warpx/map.warpy get set appropriately - so
there's no way the entity could legitimately screenwrap.
However, that happens in gamelogic(). gamelogic() is also the one
responsible for creating entities upon room load, but that happens after
the obj.customwarpmode check - so when the game gets around to rendering
in gamerender(), it sees that map.warpx or map.warpy is on, and draws
the screenwrapping, even though map.warpx/map.warpy aren't really on at
all. Only when gamelogic() is called in the frame later do map.warpx and
map.warpy finally get set to false.
To fix this, just set map.warpx and map.warpy to false when creating
warp lines.
I just spotted this one - if vy isn't bounds-checked, this causes bogus
input from the createentity() script command to commit Undefined
Behavior. Should've spotted this one when I was adding bounds checks to
the rest of createentity() earlier, but at least it's fixed now.
This makes it easier to add bounds checks to all accesses of
map.explored. Also, all manually-written existing bounds checks have
been removed, because they're going to go into the new getters and
setters.
The getter is mapclass::isexplored() and the setter is
mapclass::setexplored().
It is no longer possible to cause Undefined Behavior via accessing
out-of-bounds room properties.
What happens instead is - if you attempt to fetch an out-of-bounds room
property, you get a "blank" room property that just has all of the
defaults, plus its tileset is 1 because all tilesets that are nonzero
use tiles2.png, and it closely emulates the previous behavior where it
was some bogus value but definitely not zero. Its Direct Mode is also 1,
because the tiles contained within it are just mishmashed repeats of
existing tiles on the map, and we shouldn't autotile that.
The roomname also gets cleared in case the user attempts to set the room
name of an out-of-bounds room property.
If you attempt to set the property of an out-of-bounds room property,
then nothing happens.
This replaces all raw ed.level accesses with new setter and getter
funcs, which makes it easier to add bounds checks later. And I've also
removed all the manually-written bounds checks, since they will go into
the new getter and setter.
To get the room properties of a specific room, you use
editorclass::getroomprop(), which returns a pointer to the room
properties - then you just read off of that pointer. To set a room
property, you use editorclass::setroom<PROP>(), where <PROP> is the name
of the property. These are maintained using X macros to avoid
copy-pasting. editorclass::getroompropidx() is a helper function and
shouldn't be used directly.
This removes all traces of Undefined Behavior from getting and placing
tiles.
This mimics the previous behavior (2.2 and below) as reasonably as
possible. `vmult` was previously a vector, there was a bunch of unused
space directly after the end of the usable space of the vector, which
was all filled with zeroes. The same goes for `contents`, having
previously been a vector, and so having a bunch of zeroes immediately
following the end of the in-bounds space. That's why both are 0 if you
index them out of bounds.
This makes it easier to add bounds checks to all accesses of
ed.contents.
To do this, I've added editorclass::gettile(), editorclass::settile(),
and editorclass::getabstile() (with a helper function of
editorclass::gettileidx() that really shouldn't be used directly), and
replaced all raw accesses of ed.contents with those functions
appropriately.
This also makes the code more readable, as a side effect.
The existing bounds checks were correct sometimes but other times were
not.
The bounds check for 2x2 and 2x1 sprites only covered the top-left
sprite drawn; the other sprites could still be out of bounds. But if the
top-left sprite was out of bounds, then none of the other sprites
wouldn't be drawn - although it ought to be that the other sprites still
get attempted to be drawn. So I've updated the bounds checks
accordingly, and now an out of bounds top-left sprite won't prevent the
drawing of the rest of the sprites.
Similarly, if the sprite of a Gravitron square was out of bounds, that
would prevent its indicators from being drawn. But the indicators
weren't being bounds-checked either (2.3 lets you have less than 1200
tiles in a given tilesheet). So the bounds check has been moved to only
cover the drawframe and the indicator indexes accordingly, and an out of
bounds sprite won't prevent attempting to draw the indicators.
It is possible for any of the QueryIntAttribute()s to fail, most
commonly if the attributes don't exist. If that happens, then that part
of the temporary edentity won't be initialized, and we'll end up having
a partially-uninitialized edentity - then doing much of anything with it
will result in undefined behavior.
To fix this, just initialize the temporary edentity.
If an XML tag doesn't contain anything inside, pText will be NULL. If
that happens without being checked, then NULL will be passed to
SDL_strcmp(). SDL_strcmp() will either call libc strcmp() or use its own
implementation; both implementations will still dereference the NULL
without checking it.
This is undefined behavior, so I'm fixing it. The solution is to do what
is done with all other XML parsing functions, and to make sure pText
gets set to a safe empty string (which is just a pointer to a null
terminator) if it happens to be NULL.
PR #279 added game.gametimer solely for the editor ghosts feature. It
seems that whoever originally wrote it (Leo for the now-dead VVVVVV:
Community Edition, I believe) forgot that the game already had its own
timer, that they could use.
The game timer does increment on unfocus pause (whereas this doesn't),
but that's a separate issue, and it ought to not do that.
So #434 didn't end up solving the deltaframe flashing fully, only
reduced the chances that it could happen.
I've had the Level Complete image flash a few times when the Game Saved
text box pops up. This seems to be because the Level Complete image is
based off of the text box being at y-position 12, and the Game Saved
text box is also at y-position 12. Level Complete only gets drawn if the
text box additionally has a red channel value of 165, and the Game Saved
text box has a red channel value of 174. However, there is a check that
the text box be fully opaque first before drawing special images. So
what went wrong?
Well, after thinking about it for a while, I realized that even though
there is indeed an opaqueness check, the alpha of the text box updates
BEFORE it gets drawn. And during the deltaframes immediately after it
gets updated, the text box is considered fully opaque. It's completely
possible for the linear interpolation to end up with a red channel value
of 165 during these deltaframes, while the text box is opaque as well.
As always, it helps if you have a high refresh rate, and run the game
under 40% slowdown.
Anyways, so what's the final fix for this issue? Well, use the text box
'target' RGB values instead - its tr/tg/tb attributes instead of its
r/g/b attributes. They are not subject to interpolation and so are
completely reliable. The opaqueness check should still be kept, though,
because the target values don't account for opaqueness. And this way, we
get no more deltaframe flashes during text box fades.
An even better fix would be to not use magic RGB values to draw special
images... but that'd be something to do later.
Clang warns on this. This doesn't fix anything but it does ensure that
whoever's reading it won't be focused as to whether or not omitting the
second set of braces is legal or not.
Previously, with the wrong loop order, this kludge needed to exist so
entities in finalmode didn't have wrong colors for 1 frame when entering
a room. But now the loop order has been fixed, and so this kludge is no
longer needed.
In 2.2, at render time, the game rendered screenshakes and flashes if
their timers were above 0, and then decremented them afterwards. The
game would also update the analogue filter right before rendering it,
too.
In 2.3, this was changed so the flash and screenshake timers were
unified, and also done at the end of the frame - right before rendering
happened. This resulted in 1-frame flashes and screenshakes not
rendering at all. The other changes in this patchset don't fix this
either. The analogue filter was also in the wrong order, but that is
less of an issue than flashes and screenshakes.
So, what I've done is made the flash and screenshake timers update right
before the loop switches over to rendering, and only decrements them
when we switch back to fixed functions (after rendering). The analogue
filter is also updated right before rendering as well. This restores
1-frame flashes and screenshakes, as well as restores the correct order
of analogue filter updates.
This reintroduces 2-frame edge-flipping after the 1-frame input delay
got removed. This is because along with processing input and moving
Viridian, logical onground/onroof assignments need to processed in the
same between-render sequence as well - otherwise Viridian only gets 1
frame of edge-flipping due to frame ordering.
I will need to separate these into two different variables because I
will need to move logical onground/onroof assignments to the start of
gamelogic() - if I kept them together, however, that would change the
visuals of onground/onroof, which I want to keep consistent with 2.2.
To do this, GAMEMODE input needs to be processed, and Viridian needs to
be moved, in the same sequence between render frames. So just move
gameinput to after gamerender. Yes, this is not 2.2 order, but gameinput
only handles player input and nothing else - plus a 1-frame input delay
feels really awful to play with in over-30-mode.
In order to re-remove the 1-frame input delay, we will have to poll
input right after rendering a frame - in other words, just before an
input function gets called.
To do this, I've added a new function enum type - Func_input - that is
the same as a fixed function, but before its function gets called,
key.Poll() gets called. And all input functions have been updated to use
this enum accordingly.
This once again fixes the facing directions of crewmates upon room load,
except now it covers more cases.
So, here is the saga so far:
- 2.0 (presumably) to 2.2: crewmate direction fix is special-cased at
the end of mapclass::loadlevel(). Only covers crewmates created during
the room load, does not cover crewmates created from scripts, only
covers state 18 of crewmates.
- 2.3 currently (after #220): crewmate direction fix is moved to
entityclass::createentity(), which covers every avenue of crewmate
creation (including from scripts), but still only covers state 18.
- This commit: crewmate direction fix now covers every possible state of
the crewmate, also does not copy-paste any code.
What I've done instead is to make it so createentity() will immediately
call updateentities() on the pushed-back entity. This is kludge-y, but
is completely okay to do, because unlike other entities, crewmate
entities never change their state or have any side-effects from
double-evaluation, meaning calling updateentities() on them is
idempotent and it's okay to call their updateentities() more than once.
This does have the slight danger that if the states of crewmates were to
change in the future to no longer be idempotent, this would end up
resulting in a somewhat hard-to-track-down double-evaluation bug, but
it's worth taking that risk.
This fix is not applied to entity 14 (the supercrewmate) because it is
possible that calling updateentities() on it will immediately remove the
entity, which is not idempotent (it's changing the state of something
outside the object). Supercrewmates are a bit difficult to work with
outside of the main game anyways, and if you spawn them you could
probably just use the changedir() script command to fix their direction,
so I'm not inclined to fix this for them anyway.
This copy-pasted code only existed because the previous loop order was
incorrect and rendered entities before they would get properly updated
by the fixed render function. Now, the fixed render function is
guaranteed to be called before the render function, so we can rely on
that to update the drawframe and realcol of entities instead of
duplicating the code ourselves in createentity().
The drawframe assignment is still kept to fix the case where dying while
completestop is active (i.e. during a trinket or crewmate rescue
cutscene) and respawning in a different room won't turn everything into
Viridian sprites.
The background would change for 1 frame before sending you back to the
pause menu or editor settings. The map.nexttowercolour() call needs to
be deferred until the end of the frame.
The new loop order introduces a glitch where the menu would display
whichever menu was saved to kludge_ingametemp for 1 frame right as the
user returned to the pause menu. This happened because the
game.returntomenu() happens in titleinput(), which comes before
titlerender(). To fix this, we just need to defer it to the end of the
frame.
game.shouldreturntoeditor was added to fix a frame ordering issue that
was causing a bug where if you started playtesting in a room with a
horizontal/vertical warp background, and exited playtesting in a
different room that also had a horizontal/vertical warp background and
which was different, then the background of the room you exited in would
slowly scroll offscreen, when you re-entered the editor, instead of the
background consisting entirely of the actual background of the room.
Namely, the issue was that the game would render one more frame of
GAMEMODE after graphics.backgrounddrawn got set to false, and re-set it
to true, thus negating the background redraw, so the editor background
would be incorrect.
With defer callbacks, we can now just use a couple lines of code,
instead of having to add an extra kludge variable and putting handling
for it all over the code.
Sometimes, there needs to be code that gets ran at the end of the game
loop, otherwise rendering issues might occur. Currently, we do this by
special-casing each deferred routine (e.g. shouldreturntoeditor), but it
would be better if we could generalize this deference instead.
Deferred callbacks can be added using the DEFER_CALLBACK macro. It takes
in one argument, which is the name of a function, and that function must
be a void function that takes in no arguments. Also, due to annoying C++
quirks, void functions taking no arguments cannot be attributes of
objects (because they have an implicit `this` parameter), so it's
recommended to create each callback separately before using the
DEFER_CALLBACK macro.
Otherwise, the player would appear to "zip" during the deltaframes
between their previous position and their new position. This did not
happen in the previous game loop order and only happens in the new one.
Previously, before the game loop order got fixed, going to the in-game
settings would switch over to the new render function too early, causing
a deltaframe glitch that had to be fixed. But now, the render function
only gets switched when the current gamestate's function list gets
finished executing, so the game won't suddenly switch to titlerender()
in the middle of the ACTION press to the in-game settings screen.
As a consequence, titleupdatetextcol() no longer needs to be exported to
Input.cpp.
The previous location of this loop was placed there because it happened
just after the end of the render function. Now that the loop order is
fixed, the first thing that happens after the render function is the
start of gamelogic(), so this loop should go there now, else entity
positions won't be interpolated.
Also it now preincrements instead of postincrements because I like
preincrements.
Okay, so the reason why all render functions were moved to the end of
the frame in #220 is because it's simpler to call two fixed functions
and then a delta function instead of one fixed function, then a delta
function, and then another fixed function.
This is because fixed functions need special handling inside
deltaloop(), and you can't simply duplicate this handling after calling
a delta function. Oh, and to make matters worse, it's not always
fixed-delta-fixed, sometimes (like in MAPMODE and TELEPORTERMODE) it's
delta-fixed-fixed, so we'd need to handle that somehow too.
The solution here is to generalize the game loop and factor out each
function, instead of hardcoding it. Instead of having hardcoded
case-switches directly in the loop, I made a function that returns an
array of functions for a given gamestate, along with the number of
functions, then the game loop processes it accordingly. In fixedloop(),
it iterates over the array and executes each function until it reaches a
delta function, at which point it stops. And when it reaches the end of
the array, it goes back to the start of the array.
But anyway, if it gets to a delta function, it'll stop the loop and
finish fixedloop(). Then deltaloop() will call the delta function. And
then on the next frame, the function index will be incremented again, so
fixedloop() will call the fixed functions again.
Actually, the previous game loop was actually made up of one big loop,
with a gamestate function loop nested inside it, flanked with code that
ran at the start and end of the "big loop". This would be easy to handle
with one loop (just include the beginning and end functions with the
gamestate functions in the array), except that the gamestate functions
could suddenly be swapped out with unfocused functions (the ones that
run when you unfocus the window) at any time (well, on frame boundaries,
since key.isActive only got checked once, guarding the entire "inner
loop" - and I made sure that changing key.isActive wouldn't immediately
apply, just like the previous game loop order) - so I had to add yet
another layer of indirection, where the gamestate functions could
immediately be swapped out with the unfocused functions (while still
running the beginning and end code, because that was how the previous
loop order worked, after all).
This also fixes a regression that the game loop that #220 introduced
had, where if the fixed functions switched the gamestate, the game would
prematurely start rendering the gamestate function of the new gamestate
in the deltaframes, which was a source of some deltaframe glitches. But
fixing this is likely to just as well cause deltaframe glitches, so it'd
be better to fix this along with fixing the loop order, and only have
one round of QA to do in the end, instead of doing one round after each
change separately.
Fixes #464... but this isn't the end of the patchset. There are bugs
that need to be fixed, and kludges that need to be reverted.
Y-position 180 would be the position of the Level Complete and Game
Complete special text boxes in Flip Mode. However, since the y-position
of flipme text boxes actually no longer change (because we have to
accomodate changing Flip Mode on-the-fly), these text boxes will never
actually be y-position 180 - so we should remove these checks for
clarity.
A-ha! I've spotted an inconsistency! The normal trinket collection text
boxes (gamestate 1000-1003) is aware of Flip Mode, and will position
themselves accordingly to read the correct way in Flip Mode. However,
foundtrinket() doesn't do this.
Well, now it does.
This is why the text box attribute was named flipme, after all.
You may have noticed that the flipme command inverts textflipme instead
of simply setting it to true. Well, that's because it should be the same
as the previous behavior, which was essentially to invert it instead of
setting it to true - i.e. calling flipme twice would keep the original
text box position in Flip Mode, which means it would be upside-down
(this is a lot of flipping to keep track of...) - because flipme added
to texty in-place instead of simply assigning to it. (It did the
calculation incorrectly in 2.2 and previous, but I digress.)
Similarly, textflipme is not reset in hardreset(), because none of the
other script text box variables are reset either.
This ensures that if the player decides to toggle Flip Mode while one of
these text boxes is up, they won't be oriented improperly. Additionally,
it also de-duplicates a bunch of Flip Mode check code, which is also a
win.
createtextboxreal() is the same as createtextbox(), but with a flipme
parameter added to create text boxes that have their flipme attribute
set to true. createtextbox() just calls createtextboxreal() with flipme
set to false, and createtextboxflipme() just calls createtextboxreal()
with flipme set to true; this is because I do not want to use C++
function overloading.
Instead of calculating the y-position of the text box when it's created,
we will store a flag that says whether or not the text box should be
flipped in Flip Mode (and thus stay right-side-up), and when it comes
time to draw the text box, we will check Flip Mode and calculate the
position then.
Instead of duplicating the same variables over and over again,
Graphics::drawgui() can just make its own SDL_Rect. It's not that hard.
As far as I can tell, textrect was always being properly kept up to date
by the time Graphics::drawgui() got around to rendering
(textboxclass::resize() keeps being called a LOT), so this shouldn't be
a noticeable change from the user perspective.
The "Game Saved" text box, along with its associated telesave() call,
exists in both Game.cpp and Script.cpp, so one of them is the copy-paste
of the other. Unfortunately this copy-paste resulted in an inconsistency
where both of them don't check for the same things when deciding whether
or not the telesave should actually happen (this is why you don't
copy-paste, kids... it's scary!).
Either way, de-duplicating this now is less work for me later.
Every Level Complete sequence is the same copy-pasted thing, but with
minor changes. To make my work easier, I'm de-duplicating them so I have
less text boxes to change later, and less grind to grind.
These default arguments are never used anywhere. And if they were used
anywhere, it'd be better to explicitly say 255,255,255 than make readers
have to look at the header file to see what these default to. Also, this
creates four different overloads of createtextbox(), instead of only
two - but we ought to not be using function overloading anyway.
These commented-out code blocks just get in the way of clarity when I'm
refactoring flipped textboxes created in the gamestate system. So I'm
getting rid of them. If we need them back, we always have Git history.
Since the only difference is the y-positions, I've decided to remove the
copy-pasted code. A better solution would be to have a function that
draws multiline text and handles it accordingly in Flip Mode, but that
could be done later.
The only difference between Flip Mode and normal mode is the y-position
and sprite used to draw the crewmates. Everything else is the same, so
I've removed the copy-pasted portion.
The diff might look a bit ugly due to the unindentation.
Since the only difference in Flip Mode is the positiveness/negativeness
of the iterator variable, plus the starting y-offset, I've removed the
copy-pasted code and did this instead.
The diff might look a bit ugly due to the unindentation.
Like cutscene bars, I've added Graphics::setfade(), to ensure that no
deltaframe rendering glitches happen due to oldfadeamount not being
updated properly.
And indeed, this fixes a deltaframe rendering glitch that happens if you
return to the editor from playtesting on a faded-out screen, then fade
out again (by either re-entering playtesting and then cause a fadeout to
happen again, or by quitting from the editor afterwards). The same
glitch also happens outside of in-editor playtesting if you exit to the
menu while the screen is faded out.
To do this, I've added Graphics::setbars(), to make sure
oldcutscenebarspos always gets assigned when cutscenebarspos is. This
fixes potential deltaframe rendering issues if these two mismatch.
While working on #535, I noticed that editormenuactionpress() still
didn't do the explicit void declaration. Then I ran `rg 'void.*\(\)'`
and found three other functions that I somehow missed in #628. Whoops.
Well, now they no longer are missed.
This is a small quality-of-life tweak that makes it so if you're in the
middle of editing a level, you don't have to save the level, exit to the
menu, change whatever setting you wanted, re-enter the editor, and type
in the level name, just to change one setting. This is the same as
adding Graphic Options and Game Options to the in-game pause menu,
except for the editor, too.
To do this, I'm reusing Game::returntopausemenu() (because all of its
callers are the same callers for returning to editor settings) and
renamed it to returntoingame(), then added a variable named
ingame_editormode to Game. When we're in the options menus but still in
the editor, BOTH ingame_titlemode and ingame_editormode will be true.
This is a small quality-of-life thing that makes it so you don't have to
move your menu selection all the way over to the "return" button in
order to return to the previous menu. You can just press Escape instead
to return to the previous menu. The previous behavior of pressing Escape
was to bring up the 'confirm quit' menu, or if you were in an options
menu in-game, return to the pause menu.
If you're on the main menu (and thus don't have any previous menu) and
press Escape, the game will instead bring up the 'confirm quit' menu.
For consistency, the "quit game" option on the main menu will also bring
up the 'confirm quit' menu as well, instead of immediately closing the
game.
Pressing the controller button mapped to Escape will also work as well.
The only menus that don't have return buttons are the 'countdown' menus
- so the game will not let you press Escape if there's a menu countdown
happening.
Now that pressing Escape in the 'continue' menu will just bring you back
to the 'play' menu, there's no need to specifically put
map.nexttowercolour() first when canceling the 'confirm quit' menu.
As part of my work in #535, I've noticed that 2.3 currently with 2.2
loop order doesn't have interpolated cutscene bars. This is because
cutscene bars in 2.3 get updated at the start of the frame, which
interpolates them correctly until the render functions are put in their
proper place.
There is, however, a somewhat bigger issue, outside the scope of #535,
where cutscene bars always get updated regardless of which gamemode you
are in. Previously in 2.2 and previous, cutscene bars only got updated
in GAMEMODE and TELEPORTERMODE; sometime during 2.3, the cutscene bars
timer got pulled out of all the individual game modes and moved to the
very start of the loop. (I was probably the one who did this change;
I've been caught in a trap of my own devising.)
Thus, going to MAPMODE during the cutscene bars animation doesn't keep
their position paused like it would in 2.2. This is also categorically a
more-than-visual change, since the untilbars() script command depends
on the cutscene bars timer. I see no reason for the cutscene bars to
behave differently in this way than 2.2; #535 would also end up doing
the same fix more-or-less anyway.
Since TELEPORTERMODE currently uses the same renderfixed function as
MAPMODE, I've had to add a teleporterrenderfixed() that just calls
maprenderfixed(), but also does the cutscene bars timer.
As a partial fix for #618, adding the SDL2 version number to the README
will clarify that you need a specific version of SDL2 in order to
compile (and run) the current version of the game (2.3 at the time of
writing); in the future, the SDL2 dependency will be upgraded with each
SDL release.
This is to avoid error messages that complain about missing symbols like
SDL_zeroa() (added in SDL 2.0.14) not being present at the time of
compilation.
Closes#626.
This moves the responsibility of toggling fullscreen when any of the
three toggle fullscreen keybinds are pressed (F11, Alt+Enter, Alt+F)
directly into key.Poll() itself, and not its caller (which is main() -
more specifically, fixedloop()). Furthermore, the fullscreen toggle
itself has been moved to a separate function that key.Poll() just calls,
to prevent cluttering key.Poll() with more business logic (the function
is already quite big enough as it is).
As part of my work in re-removing the 1-frame input delay in #535, I'm
moving the callsite of key.Poll() around, and I don't want to have to
lug this block of code around with it. I'd rather refactor it upfront
than touch any more lines than necessary in that PR.
This fixes a bug where the resumemusic() script command would always
play MMMMMM track 15 (or, if you're using PPPPPP, just not work). This
is because musicclass::haltdasmusik() assigns resumesong AFTER calling
Mix_HaltMusic(), but the songend() callback fires before the resumesong
assignment, meaning resumesong gets set to -1 instead of whatever
currentsong was previously.
To fix this, just move the assignment into the callback itself (I don't
know why this wasn't done before). I could have moved it to before the
Mix_HaltMusic() call, but moving it into the callback itself fixes it
for all cases of the music stopping (such as when the music fades out).
This avoids the room name awkwardly moving back up if the cursor is at
the bottom of the screen in a room with a room name, then the user
switches to a room without a room name, then moves the cursor away from
the bottom, then switches to a named room - even though the cursor was
already away from the bottom of the screen.
Conversely, if the user moves their cursor to the bottom of the screen
in an unnamed room, then switches into a named room, the room name will
already have been hidden and they won't need to wait for it to hide.
This fixes the drawer suddenly popping up only to disappear, if the user
leaves a Direct Mode room into a non-Direct Mode room when the drawer
hasn't closed all the way, and then re-enters a Direct Mode room.
Gravity line correction no longer happens on every deltaframe. This
means less CPU time is wasted. Although, there's probably no need to
correct gravity lines on every single frame... hm... well, that's an
optimization for later (there's plenty of other stuff to cache, like
minimap drawing or editor foreground drawing).
Since it only ever gets assigned from FILESYSTEM_getUserSaveDirectory(),
and that function returns a C string, and the variable is only ever read
from again, this doesn't need to be an std::string.
In #553, when Dav999 added error messages to settings menus if the game
was unable to successfully save the changed settings, he seemed to have
forgotten the PPPPPP/MMMMMM toggle option.
However, I can fully blame him for only that miss. The Flip Mode options
were using game.savemystats (which was removed in #591), so if he
searched for all instances of game.savestats()
(game.savestatsandsettings() was only added in #557), he would've missed
the game.savemystats.
Later, when I did #591, I didn't realize that I should've replaced the
ones in the Flip Mode options with game.savestatsandsettings_menu(), so
part of the blame does fall on me.
Anyways, this is fixed now.