This command adds `ifversion`, as both a simplified and internal
command.
The system works like so:
- `ifversion(2.6,script2)` -> If you're on 2.6.0 or greater
- `ifversion(2.6.0,script2)` -> Same as above
- `ifversion(2.6.3,script2)` -> You're using 2.6.3 or greater
- `ifversion(2.5,script2)` -> You're using 2.5 or greater
- `ifversion(2,script2)` -> You're using 2.0.0 or greater (yep...)
`ReleaseVersion.h` has a few new defines to make this possible, being
`MAJOR_VERSION`, `MINOR_VERSION` and `PATCH_VERSION`. With the help of
a few macros, `RELEASE_VERSION` is now constructed using those.
Ethan mentioned how case sensitivity could be a concern -- this commit
lowers the filename to create the sound ID, and also strips spaces.
Script arguments are always lowered, and the spaces get removed, so the
IDs are modified to match this behavior. If someone types in the
command `playef(horses REALLY COOL)`, the argument will get converted
to `horsesreallycool`. Likewise, the filename "horses REALLY COOL.wav"
will get converted to `horsesreallycool`. To the level creator, they
can just enter the same name twice and their sound will play.
This commit also adds a warning for the `playef` script command. While
most creators do not open the console, some warning somewhere is
better than nothing. I do think we should eventually have some sort
of system where it's easier to view level warnings, but for now this
should be sufficient.
This commit allows the usage of new "extra" sounds in levels for use in
custom level scripting. Including `squid.ogg` inside of your sounds
folder will now allow for the usage of the command `playef(squid)`.
Additionally, you can play built-in sounds by name this way too,
instead of having to memorize the id (`playef(rescue)`).
However, you CANNOT play extra sounds through using their numerical
IDs, since those can shift around depending on the filesystem (or if
someone adds a new sound). Playing an extra sound by ID will NOT play
the sound.
This is another attempt at #902, being loosely based off of that PR.
Co-authored-by: Fussmatte <doormattion@gmail.com>
Fixes#1242.
Turns out it was a really simple fix - the X positions were good, but
the Y positions were always at the top of the screen regardless of the
height of the textbox. Now they're vertically centered respective to
the speaker.
This fixes our problem with Valgrind reporting a jump based on an
uninitialized value; see https://github.com/Tehreer/SheenBidi/issues/19
Just to be sure nothing unexpected happens, I tested that this doesn't
cause any changes in behavior by outputting bidi-transformed versions
of all strings in strings.xml to a file for both our Arabic and Persian
localizations before and after the update, and confirming that the
files are the same.
This includes translations that were missing translations, with varying
extent between different languages, for the following things:
- "X mode is enabled" in-game warnings
- "Press {button} to freeze/unfreeze gameplay" for the level debugger
- Some credits strings for the post-2.4.0 extra Spanish options, the
PT_BR proofread, and Persian
- The recent gamepad menu changes (#1229)
Furthermore:
- "TAB" (used in the level debugger string) is now a separate string
instead of being hardcoded, because some languages needed it
translated
- Added missing arrows to Arabic/Persian font (needed for the gamepad
menu, and also a translator menu actually)
Seems like the build root can change and CMake rightfully gets confused at the conflicting paths, so let's just use the minutes to do this from scratch.
- Update actions/checkout to v4
- Add build caching for macOS and Linux jobs
- Implement concurrency control to cancel redundant runs
- Restrict GITHUB_TOKEN permissions for security
These changes improve CI performance, stability, and security.
When writing the initial stretch mode code, the existence of HiDPI
displays completely slipped my mind -- or at least I didn't realize
that they'd be a problem.
We implement scaling modes ourselves, so transforming the mouse
coordinates to our 320x240 viewport is done manually. Unfortunately,
that code did not take into account HiDPI scaling whatsoever, meaning
that all of the math which assumes the window size and the renderer
size is wrong.
To fix this, we use `SDL_GetWindowSizeInPixels` and `SDL_GetWindow` to
find the scaling factor, and then apply that to the mouse coordinates
to get the mouse coordinates in the pixel-space instead, and then we do
all of our normal logic after.
Due to our usage of `SDL_GetWindowSizeInPixels`, this bumps the minimum
SDL version to 2.26.0.
Closes#1235.
A SDL_GameControllerButton below 0 is out of array bounds, so that
should trip the assert and return GLYPH_UNKNOWN just like when the
value is too high.
This makes the following improvements to the gamepad bindings menu:
- The menu now shows a hint that you can press a button while any of
the bind options are selected (or that you can navigate away from
those options)
- Instead of button presses immediately setting a binding, they now
ask for confirmation: press the same button a second time to confirm
- You can now remove a binding, the same way you add it (this has the
same type of confirmation)
- This menu used to be inconsistent with pretty much every other menu
in the game by showing a permanent title and description for the menu
itself ("Game Pad", "Change controller options.") rather than showing
a title and description for the currently selected option.
This inconsistency is now fixed.
The Persian localizers noticed an issue where the translation for
"Dimension VVVVVV, 12:34:56" would become "VVVVVV, 12:34:56 noisnemiD",
rather than "12:34:56 ,VVVVVV noisnemiD" as expected. This was fixed
for Persian but the same issue also affected Arabic, so I added a
RIGHT-TO-LEFT MARK (U+200F) to fix it there as well.
This adds "Persian" to all the languages, and brings the Persian files
up-to-date (mainly removing outdated strings and adding some late 2.4
stuff that we'll contact all translators for soon)
Every now and then, the game crashes for me because of a division by
zero, due to the rect returned by `Graphics::get_stretch_info`. I don't
fully know how the width and height get set to 0, but this should
protect against it.
As I always use integer scaling, my guess is that
`Screen::GetScreenSize` (which later calls `SDL_GetRendererOutputSize`)
returns 0 sometimes, and the code trusts that -- but I know that
windows and things can be finicky, so the clamp is probably a good
idea.
When `SDL_RenderReadPixels` fails for some reason, the game tries to
free the temporary source surface it creates. Unfortunately, it never
sets it to `NULL` after, so the next time the game tries to render the
filter, it'll try to work with a memory region that was already freed.
To fix this, I just replaced `SDL_FreeSurface(*src);` with
`VVV_freefunc(SDL_FreeSurface, *src);` which is a helper macro which
sets the pointer to NULL after freeing.
Now, there's a new issue -- since the temporary buffer is now NULL,
next frame we'll try to remake it! So I've introduced a static bool
which disables the filter entirely if `SDL_RenderReadPixels` fails.
Without this, it'd create and destroy a surface every frame, which
could lead to slowdown. (Not as slow as the filter when it DOES work,
but still...)
I also added a line which frees the second temporary surface... it's
weird that was missing in the first place, but I think reimplementing
analogue mode was one of the last things I did for the renderer
rewrite anyways.
Resolves#1223.
PR #1226 tweaks both the crew and the stats screens. When there's no
trinkets in the level, it removes the trinket count from the STATS
screen in the menu. This, however, is missing a `custommode` check,
meaning that the main game is ALSO missing the trinket count.
This PR fixes that oversight.
There's a problem in #1224 where it breaks spawning custom activity
zones. After a bit of confusion after this was reported, I realized
that I removed the fallback from `getcrewman` and changed the return
value to `-1` if the entity isn't found, to avoid returning the wrong
entity (entity 0, aka probably the player).
Unfortunately, it seems like a ton of levels (including my older ones)
rely on this behavior.
Creating custom activity zones is a long process which uses a bunch of
unintended behaviour, which includes targeting a crewmate with color
35. With the change I mentioned earlier, the `getcrewman` function
would return `-1` instead, which was out of bounds of the entity array,
so the game avoided spawning the activity zone at all. The prior
behaviour of falling back to entity 0 (most likely the player) would
spawn the activity zone around the player instead.
Nowadays, I try to spawn a crewmate with color 35 anyways so I can
control where the box spawns (instead of on the player always), however
most people don't (and haven't) so reverting this change seems best for
now.
If we wanted to reintroduce the `-1` fallback in the future, things
that call `getcrewman` would have to check for `-1` and use `0`
instead, but that would require a lot more testing and studying where
it's used, and I'd rather squash this bug quickly and worry about
cleanliness later.
Entity colors are just integers. Their colour ID gets passed through a
big switch, returning different RGB values depending on the colour ID
(and can also get affected by randomness or other game state.)
But because of this, there's a bunch of random numbers floating around,
with no actual description on what they are other than comments which,
while most of the time are accurate, only exist in the switch.
To fix this, this commit adds a new enum which labels every colour.
While we can't use it as a type (as we need to allow colours outside of
what are defined, in case people want a "pure white", and scripting can
set any colour ID they want), colours have to stay as `int`.
This PR adds a new XML property for the player's colour. It is 0 by
default, but you can change it to any colour ID. For example, making
the player use the trinket color is `<PlayerColour>3</PlayerColour>`.
This is mostly a quality-of-life addition, as the player's colour is
always 0 unless changed by scripts. A lot of levels which use different
player colours use an intro script which both changes the player's
colour and sets their respawn colour, which works great for finished,
completed levels, but makes playtesting a little more annoying as they
will spawn in as the wrong colour. Adding a level property for the
default player colour fixes this annoyance.
Additionally, this changes the behavior of `restoreplayercolour`. This
command used to set the player's colour to 0 (ignoring the respawn
colour, so when Viridian would die, they would revert to the respawn
colour). Now, it sets both Viridian's colour AND the respawn colour to
what was present in the level file. This way, you can temporarily
change the player colour using the script commands, and then use
`restoreplayercolour` to revert back to what the player colour
normally is.
The start point colour has also changed, to show the player colour
instead of always colour 0.
Like most changes like this, a way to change this in-editor does not
yet exist, but is planned for the future.
Turns out there was a little miscommunication with the translator -
they said the option needed to say "Ślōnsko" but were only talking
about correcting the first word, where I thought the whole thing needed
to be replaced.
If you push a button to set a controller binding, you may either hear
one Viridian squeak, two Viridian squeaks (a louder one), or Viridian
doesn't stop squeaking until you let go of the button. While you hear
the continuous squeaking, your save file is also repeatedly saved.
There are two small bugs at play here:
- the squeak is actually played in two different places at the same
time (both in titleinput() whenever a button is pressed, and in
updatebuttonmappings() when a mapping is succesfully changed)
- titleinput() doesn't register that a button is held down and applies
the button (and saves to file) every frame for as long as the button
is held
This commit fixes both these issues. Now a single button press always
causes one squeak, and only if the bindings actually changed. Your save
file is also no longer saved repeatedly from holding down the button.
In an effort to remove magic numbers, I've given every entity type a
name. Hopefully I didn't miss anywhere.
Also, add `createentity` case 100 for backwards compatibility.
Co-authored-by: NyakoFox <nyakowofox@gmail.com>
Co-authored-by: Dav999 <dav999.tolp@gmail.com>
This argument forces the textbox position, meaning it won't be moved
to be inside of the bounds of the screen (nor have the 10 pixel padding
on each side.)
This replaces all instances of unlocking all rooms on the map with calls
to map.fullmap(), for consistency.
This also fixes two comments that got swapped around in startgamemode().
I don't know how that happened.
[skip ci]
Some discussion on the Discord server resulted in this change. It's a
quality-of-life improvement where, if the game is in slowdown mode, it
will return to 100% speed for the duration of the death animation.
The reasoning is obvious. There is nothing to do during the death
animation, so making it take longer during slowdown is just an annoyance
to the player, almost a penalty for them using an accessibility option.
This is the same reason why slowdown no longer applies in menus, etc.
This makes it so that it is possible to obtain the Master of the
Universe trophy/achievement, usually unlocked by beating No Death Mode,
outside of NDM.
There are several conditions that need to be met:
1. The game needs to be started from a new game and cannot be from
loading a save.
2. Accessibility modes (invincibility and slowdown) must never be
enabled.
If either condition is violated, then the boolean that keeps track of
NDM eligibility will be set to false.
Currently, you can change platform speed, but not enemy speed, which is
always hardcoded to be 4. This commit fixes that, by adding the
"enemyv" property, which is an offset to the speed of 4. Since it
defaults to 0, older levels are not broken by this change.
Just extending the selection background left by one pixel so there's
not one pixel of black background to the left of a selection that
starts at the beginning of the text, and so some characters being
selected show up better (particularly where there's a long vertical bar
at the first pixel). We shouldn't be overlapping any part of the
previous character, since every character normally has a pixel of
spacing on the right.
Decided to implement it anyway since the broken behavior (selection
length always being 0, at least on Windows) may get fixed later in SDL,
so let's do it right in one go.
This shows the uncommitted text in a box in the bottom left corner.
This doesn't show the selection (defined by the start and length fields
in the event) yet, but this is already much better than it was on its
own, and I don't know how urgent the selection is since it's broken on
Windows anyway.
When inputting uncommitted text from an IME, this is now stored in a
std::string imebuffer, just like keybuffer. It also enables extended
editing events, so text longer than what fits in the standard editing
event is also supported. This commit does not yet display the text
onscreen.
This fixes a regression from 2.3. Consider the following diagram:
CC
X CC
<<<<
"C" indicates one tile of a checkpoint entity, "X" indicates a spike
tile, and "<" indicates one tile of a conveyor entity that has the
default speed (4 pixels per frame) going leftwards.
Now consider if the player were to touch the checkpoint and die. In 2.2,
they would be able to escape from the spike by holding right. But in
2.3, they would not be able to, and would die from the spike
continuously with no escape.
This happens because in 2.2, the player would spawn a couple pixels off
the surface, and this gained them an extra frame of movement to move
away from the conveyor. 2.3 places the player directly on the ground,
moving them one frame earlier and thus forcing them to their doom.
Now consider the following diagram:
CC
X CC
<<<<
The difference with the previous diagram is that this time, the spike is
one tile closer. This time, there is no escape in 2.2 and you will
always die no matter what.
By the way, both diagrams have the same behavior if the conveyor is
rightwards and if everything is flipped upside-down. Thankfully, it
doesn't seem to be direction-dependent.
The reason 2.3 lowered the player onto the surface was for consistency
(see PR #502), and I don't want to undo that. So I think the best
solution here is to re-add the extra frame of control by conveyors only
moving the player after lifeseq has advanced enough.
This fixes a bug where fast-forward wouldn't work in 30-FPS-only mode.
This is because the 30-FPS-only code has a hardcoded check for the
number 34, as in 34 milliseconds must pass before the next frame can
advance. This is why slowdown still worked, because slowdown means
you're waiting longer than 34 ms anyways, but fast-forward tries to wait
for only 1 ms, which wouldn't work if the 34 limit was still enforced.
So instead, swap out the 34 with game.get_timestep() and this will be
fixed.
Fixes#1185.
This fixes an issue where the CentOS CI kept failing because it couldn't
find the generated InterimVersion output file.
It seems like using the BYPRODUCTS statement in add_custom_target
didn't work because BYPRODUCTS was only added in CMake 3.2, so then
add_custom_target never ran, which is obviously a problem.
The solution is to use add_custom_command instead, and to solve the
problem that the interim version needs to be regenerated every time no
matter what (which is what BYPRODUCTS was supposed to do) we just add a
dummy output instead.
Previously, the interim version indicators (commit, date, and branch)
would go away on development builds if git didn't exist. And if it did
exist but provided blank output for whatever reason, that was almost
exactly the same as not having them at all (save for the window title
saying "VVVVVV []" which can be easy to overlook). This was bad because
it complicates troubleshooting when you potentially have an unofficial
or in-development build since those get distributed around or compiled
by some people frequently.
Now, there will always be an interim version indicators unless the game
is compiled with -DOFFICIAL_BUILD=ON. And if the indicators are blank
for any reason, they will just be replaced with placeholder defaults so
they will still show up.
GCC on CentOS will default to C90, it seems. This means it needs C99
explicitly specified for C-Hashmap and FAudio, or it will fail on them
using C99 features (variable declaration in a `for`-loop and the
`restrict` keyword, respectively).
Due to a confluence of weird factors, it turns out that PhysFS is
compiling with implicit function definitions due to function definitions
that get hidden with -std=c99, but not with -std=gnu99 (or the default
GCC value of -std=gnu17).
Also, due to a recent GCC update (GCC 14), implicit function
declarations are actually prohibited with -std=c99 as the C99 standard
proscribes.
This meant that people started getting build errors in PhysFS code on
default settings, which wasn't ideal.
To fix this, we will make our -std= flags apply only to VVVVVV source
files. In CMake 2.8.12, this can be done with
set_source_files_properties. Additionally the flags to disable
exceptions and RTTI are scoped down too.
Thanks to leo60228 for helping debug and solve this issue.
Fixes#1167.
A minor gripe, but one thing I didn't notice with commit
b4579d88d3 is that it now results in two
dialogs if data.zip is missing: The first being the "data.zip is
missing" dialog, and the second is the generic "Unable to initialize
filesystem" dialog.
So just bail early if data.zip can't be found, it's going to take the
error path in main() and also bail regardless.
`/EHsc` does not actually disable exceptions on MSVC, it only makes the
compiler assume that `extern "C"` functions never throw C++ exceptions.
We had a discussion on Discord about actually disabling exceptions, and
from research, that requires defining `_HAS_EXCEPTIONS=0`, but it's
unsupported and undocumented so we deemed the benefits not worth it.
Thus, we will stay with `/EHsc`. But the comment still has to be
updated.
[skip ci]
lang/README-programmers.txt accidentally lists the name of the
font::print function twice, when the second one should've been
font::print_wrap instead. Oops.
[skip ci]
Commit 53d725f78a, intended to fix an
overzealous commit, was itself overzealous. This is because it applied
to all entities when it should only apply to entity-emitting entities.
To fix this, `entityclonefix` needs to no-op if the entity is not an
entity emitter.
Fixes#1176.
Commit 4f881b9e26 fixed a duplication bug
where enemy movement types 10 and 12 would keep duplicating itself on
every frame if it was spawned outside of the rooms they were supposed to
be used in the main game. The downside was that this was an overzealous
fix and unintentionally broke some cases that were working before.
As brought to my attention by Ally, you can no longer place an edentity
with a `p1` of 10 or 12 (translating to movement type 10 or 12) in the
proper rooms and have it spawn perfectly working entities (that don't
clone on themselves every frame), whereas you could in 2.2. This is
considered a regression from 2.3.
So the problem here is that the reason the two emitter entities were so
dangerous outside their respective rooms is because the entity they
spawned (`createentity` entry 1) checked if it was in the correct rooms,
and if so, it would call `setenemy`, and `setenemy` would set the
`behave` attribute (movement type) correctly, and so the new entity
would have a different `behave` that wouldn't be the exact same `behave`
as the previous one, so it wouldn't be a duplicate emitter entity.
The previous `entityclonefix` worked okay for entry 1, because it would
only be run if the room checks failed and `setenemy` wasn't called, but
it broke a previously-working case for entry 56, because it was always
run for entry 56.
So the best way to check if we have a dangerous entity is not by seeing
if it is still `behave` 10 or 12 at the end of entity creation - because
10 or 12 could be harmless under the right conditions - but by checking
if the right conditions were satisfied, and if not, then neutralize the
entity.
I considered making the emitter entities work everywhere - which would
be simpler - but I didn't want to go too far and add a new feature,
especially in a minor release.
This fixes a minor issue where if you had a zip in the levels list, but
then removed it, it would still show up in the levels list after
reloading it (if you also had a .vvvvvv file named the same as in the
zip) even though it shouldn't.
Thankfully, this didn't lead to a memory leak or duplicate zip mounts or
anything like that, because PhysFS ignores mounting a zip if it's
already mounted.
This also didn't result in a level entry from a zip persisting after
removal after reloading the levels list, because the entry would be gone
due to the .vvvvvv file not being found.
The intention of the `-console` argument was to enable seeing console
output on Windows without having to use workarounds. However, this
didn't actually work for arguments like `-addresses` and `-version`,
because the program would exit first before it could get the chance to
create the console.
The other issue is that the console closes too quickly before output can
be read by the user. So to fix that, we must hold it open and let the
user close it when they want to by waiting for an enter press from
STDIN.
This fixes a regression from 2.4 where the foreground wouldn't update
after using the G keybind to go to a room, requiring the user to touch a
tile to update the rendering.
This fixes a bug report from Elomavi that you could still softlock from
warping to ship and incrementing the gamestate by pressing ACTION, which
is diverging behavior from how it was in 2.3. Warping to ship and
incrementing by pressing ACTION is useful behavior for a couple niche
speedrun categories.
I had already fixed this earlier by ignoring state locking if
glitchrunner 2.2 or 2.0 was enabled, but softlocks could still happen
because having glitchrunner mode off still enabled you to increment the
gamestate when otherwise unintended. Softlocks shouldn't happen.
But without removing state locking entirely, I've chosen a middle ground
where it will only be disabled if you press ACTION. That signifies
intent that you still want to perform state incrementing glitches even
with glitchrunner mode off (but in the future it could be considered a
2.3/2.4 glitch that could be patched and made re-enable-able). That way,
casual players can't interrupt the warp to ship by accident (unless they
accidentally press ACTION) while softlocks will be removed.
For localization, the "Thanks for playing!" text was split into two
lines, when it was originally one line. Unfortunately, it was not
updated to account for Flip Mode, so in Flip Mode, it looked like
"playing! Thanks for".
This has been fixed.
If there was a scaling mode value (serialized in the XML as <stretch>
for legacy reasons) that was not 0 or 1 or 2, then the rectangle with
the stretch information would not be initialized by get_stretch_info,
which would lead to a crash, either from dividing by zero (most likely)
or from reading an uninitialized value.
To fix this, when reading <stretch>, normalize it to a sane default if
the value is otherwise bogus. And for good measure, an assertion is
added in get_stretch_info() if the value is still somehow bogus.
Fixes#1155.
This is just a small visual fix to an inconsistency with textbox
colors in simplified scripting. The `reply` command is meant to be
used for the player, and always correctly positions it above the
player, while the `say` command may be used to generate a cyan textbox
that's positioned above a cyan non-player crewmate. However, the color
for both textboxes is always `cyan`, so the `reply` command doesn't use
the (normally identical) `player` color even though all its other
behavior (squeak, position) does. Now that customized textbox colors
were added in 2.4 (#910), it's a shame that this distinction isn't
made between `cyan` and `player`, so this change addresses that (before
we're stuck with levels that change `cyan` but not `player`).
After some discussion about the previous commit, the usecase of
managing tons of basedirs and locking files in the filesystem might
mean it gets annoying to have the language screen show up again
whenever a new language is added, for a small group of people. The
solution to get the best of both worlds is to only re-ask for the
language in the default basedir. This means barely anyone will miss
their language having been newly added (especially since barely anyone
will use any custom basedirs, let alone ONLY custom ones).
Now that two new variants of Spanish have been added, it would be
a shame that many players from Latin-America/Argentina may stay on
Castilian or English because they don't realize the new versions
were added for them. So now, if you've set your language in 2.4.0,
the language screen will show up once more in 2.4.1. This is done by
simply incrementing the lang_set flag to 2 - so that if it's 0 or 1,
your language setting is considered to be possibly outdated.
This shouldn't inconvenience players who don't need to select a new
language - their existing language will still be pre-selected, so they
can just hit ACTION once.
Terry confirms he did the same thing with Dicey Dungeons and says
it's a good idea (and that nobody minds).
This fixes the possibility of the "resize to nearest" graphics option
resizing the game window to be bigger than the resolution of the user's
desktop monitor.
To fix this, just subtract multiples of 320x240 until the chosen
multiple is smaller than the dimensions of the desktop.
Discord user Dzhake discovered this issue.
All of them were changed except for the one in meta.xml. I think it's
safe to assume this is correct, because everywhere else, the same
"Oprime {button} para" pattern always became "Pulsa {button} para" too.
For future PRs, it'll be very nice to have full control over how VVVVVV
gets drawn to the window. This means we can use the entire window size
for things like touch input, drawing borders, or anything we want.
They were missing the latest strings and also still had strings that
had been deleted.
(Whoever commits the upcoming delivery should also sync that version)
This is so they will be updated when switching language with CTRL+F8.
Most of the editor notes are simple text that don't use any string
formatting. For the ones that aren't, some (saving and loading, changing
map size) reference variables that wouldn't change without initiating a
new note anyway. For the others, i.e. the ones that _do_ reference
variables that could easily be changed (tileset name, speed) by
switching the current room, we cache their values and use the cached
values when drawing the note. Unfortunately, this requires adding a
couple of ugly attributes to editorclass, but it'll be fine.
These are simple strings (no vformat), so we can just un-bake them to
make sure that cycling languages with one of them onscreen updates them
accordingly.
These weren't getting updated when cycling language with CTRL+F8. This
is because they would be already baked. Luckily, at least the bool
keeping track of whether or not to translate them in the first place
already exists, so we can just rely on that.
This makes it work pretty well. It basically just resets the state of
the limits check and starts from the first limit broken (if any), which
is behavior that makes sense to me.
Otherwise, without this, it seems to invalidate pointers and, on my
machine, start pulling strings from the language XML, which is
horrifying.
Not gonna lie, I am a bit disappointed at having to do this, because it
actually worked pretty well despite a few bugs depending on which
language you entered with. But that's only because I'm working with
the official translation files, which are in sync with each other.
With translation files that are completely arbitrary, it would be
apparent that switching languages during the cutscene test doesn't
really make sense. Like, at all. That's because the list of cutscenes is
populated entirely from language-specific XML and the cutscenes in them
are also from language-specific XML. So keeping the same position in the
menu doesn't really make sense, and keeping the same position in a
cutscene definitely doesn't make sense.
I saw that the only problem with cycling languages in a title screen
menu is that the menu options don't get updated. So I was like, we can
just recreate the menu, and then I was like "Sure, why not." So that's
what I did.
To accommodate the CTRL+F8 keybind in the language menu, it
automatically updates the menu option when you cycle it. This is because
otherwise using the keybind in the language menu wouldn't visibly update
the language, but it still actually does change your language, and that
can be seen by pressing Escape.
Also, the menucountdown needs to be preserved because otherwise
createmenu() resets it, even if it's the "same" menu (this behavior is
needed so that the menu that is shown during the countdown isn't added
as a stack frame which would make it a menu that could be returned to).
Originally, textcase was reset in scriptclass::translate_dialogue(),
which is called inside the `text` script command. However, this didn't
really work with the new on-the-fly text box translation system, and
that function is gone now, so I removed that and kind of forgot about
it.
Of course, this now causes a regression. Namely, that the text boxes
after the VVVVVV-Man sequence in the Secret Lab entrance cutscene are
not translated.
I can't reset the text case in `text`, as the scripts assume that they
can set the text case before `text`. So the next best thing is to reset
it in speak/speak_active.
This fixes a bug where some text boxes wouldn't update the displayed
button if the active input device changed from a keyboard to controller,
or vice versa. Namely, the "Press ACTION to continue" text boxes.
This removes Graphics::textboxwrap(), as it is now an unused function.
Additionally, this removes the return value of textboxclass::wrap(), as
it is also now unused.
This stores the original x-position and y-position of the text box, and
when a text box gets repositioned, it will use those unless a crewmate
position overrides it.
This is the original position of the text box, before centering or
crewmate position is considered.
This fixes a bug where a cutscene text box can be "shifted" from its
normal position via CTRL+F8 cycling if there is a translation that is
too long for the screen and thus gets pushed by adjust(). I tested this
with the text box in the Comms Relay cutscene that starts with "If YOU
can find a teleporter".
This is not applicable to function-based translations
(TEXTTRANSLATE_FUNCTION), because the responsibility of correctly
positioning the text box resides with the function.
This adds a debug keybind to cycle the current language forwards,
CTRL+F8. It also adds a debug keybind to cycle it backwards,
CTRL+SHIFT+F8.
This is only active if the translator menu is active (and so if the
regular F8 keybind is also active).
This is useful for quickly catching errors in translations and/or
inconsistencies between translations. In fact, I've already caught
several translation mistakes using this keybind which made me mildly
panic that I screwed something up in my own code, only to realize that
no, actually, it was the translation that was at fault.
For now, this is only meant to be used in-game, as text boxes get
retranslated instantly, whereas things like menu options don't. But menu
options will be retranslated on-the-fly in a later commit.
This fixes a problem where it would incorrectly format the text because
the width of the text box hadn't updated yet.
This fixes a bug where the jukebox informational terminal would
initially be created with too much padding in CJK languages, pushing the
text box offscreen, even though switching languages while the text box
is already open fixes it.
In order to be able to retranslate the game time text box in particular,
I had to create new variables to bake the saved time, since the existing
savetime variable is just an std::string. From there, the saved time can
be retranslated on-the-fly.
This adds an attribute to textboxclass to allow a text box to keep an
index that references another text box inside the graphics.textboxes
std::vector.
This is needed because the second text box of a "You have found a
shiny trinket!" or "You have found a lost crewmate!" pair of text boxes
explicitly relies on the height of the first text box. With this, I have
moved those text boxes over to the new text box translation system.
Since the update order now matters, I added a comment to
recomputetextboxes() that clarifies that the text boxes must be updated
in linear order, starting from 0.
This adds an assert to Graphics::textboxtranslate() to make sure that
callers don't accidentally provide a function when specifying a
translation type that isn't TEXTTRANSLATE_FUNCTION, because in that case
the function won't be used, and then it will make them scratch their
heads wondering why their function won't work.
And yes, I am stupid enough to blindly type TEXTTRANSLATE_CUTSCENE when
I meant to type TEXTTRANSLATE_FUNCTION. This assert has already caught
one of my mistakes. :)
These seemed annoying to do without copy-pasting, because I didn't want
to make a separate function for every single dialogue, and I didn't know
how to pass through the English text, until I realized that I can just
use the existing original.lines vector in the text box to store the
English text. After that, getting it translated on-the-fly isn't too
bad.
Just a small optimization.
For example, consider the calls in adjust(). After the first resize(),
the lines after only change the x-position and y-position of the text
box and depend on the x-position, y-position, width, and height.
However, resize() only changes the width and height if the contents of
the text box change, which after the first call, they don't. So remove
the second call to resize(), because it's completely unnecessary.
By similar reasoning, the second calls to resize() in centerx() and
centery() are unnecessary too.
This transfers the responsibility of the adjust() call to
applyposition().
This is because cutscene text boxes (TEXTTRANSLATE_CUTSCENE) will have
adjust() called, but all other text boxes won't. And I can't place the
adjust() call inside applyposition(), because adjust() also calls
applyposition(), and that leads to an infinite loop which leads to a
stack overflow, so I had to remove the applyposition() call from
adjust(), and replace the other existing call to
Graphics::textboxadjust() with Graphics::textboxapplyposition(), and
then remove Graphics::textboxadjust() function because it's no longer
used.
Several text boxes in the gamestate system are unused and are
untranslated. To prevent them from becoming empty when retranslating
text boxes, we need to save their original context by calling
graphics.textboxoriginalcontextauto() (which is just
graphics.textboxoriginalcontext() but automatically saving whatever is
already in the text box at the time).
With the new system of retranslating text boxes on-the-fly, this also
enables us to retranslate them whenever the player toggles Flip Mode.
This is relevant because the Intermission 1 instructional text boxes
refer to a floor when Flip Mode is off, but when it is on, it talks
about the ceiling.
This splits the text wrapping functionality of Graphics::textboxwrap to
a new function textboxclass::wrap, and with this new function, some more
text boxes can be moved to the new TEXTTRANSLATE_FUNCTION system.
Namely, Game Saved (specifically the game failing to save text box),
instructional text boxes in Space Station 1, and the Intermission 1
instructional text boxes.
This adds a way to save the text box state of the crew remaining, ACTION
prompt, etc. text boxes by just letting there be a function that is
called to retranslate the text box when needed.
It also adds a way to ignore translating a text box and to leave it
alone, in case there's actually no text in the text box, which is the
case with Level Complete and Game Complete.
Both ways are now in an enum, TextboxTranslate. The former is
TEXTTRANSLATE_FUNCTION and the latter is TEXTTRANSLATE_NONE. The
existing way of translating text boxes became TEXTTRANSLATE_CUTSCENE,
since it's only used for cutscene scripts.
Here's a quick guide to the three ways of creating a text box now.
- TEXTTRANSLATE_NONE: You must call
graphics.textboxoriginalcontextauto() to save the existing text to the
original context of the text box, as that will be copied back to the
text box after the text of the text box is updated due to not having a
translation.
- TEXTTRANSLATE_CUTSCENE: Translates the text from cutscenes.xml, and
overrides the spacing (padding and text centering). Shouldn't need to
be used outside of scriptclass.
- TEXTTRANSLATE_FUNCTION: You must pass in a function that takes in a
single parameter, a pointer to the textboxclass object to be modified.
General advice when retranslating text is to clear the `lines` vector
and then push_back the retranslated text. The function is also solely
responsible for spacing.
In most cases, you will also need to call
graphics.textboxapplyposition() or graphics.textboxadjust() afterwards.
(Some text boxes shouldn't use graphics.textboxadjust() as they are
within the 10-pixel inner border around the screen that
textboxclass::adjust tries to push the text box out of.)
This commit doesn't fix every text box just yet, though. But it fixes
the Level Complete, Game Complete, crew remaining, and ACTION prompt
text boxes, for a start.
This is another piece of state that needs to be kept and re-played when
switching language, because a different language could change the
dimensions of the text box, which affects how it's centered.
Also, to make sure that crewmate positions override any text centering,
the scriptclass variables textx and texty should be reset in the
position and customposition commands.
Originally I did a straight deep copy of the original lines, but this
ignores the limit of either 12 or 26 lines in a text box. So we defer to
addline() which will enforce the limit accordingly, just like it would
do with the original text box.
This allows switching languages while a text box is on screen by saving
the necessary state for a text box to be retranslated when the language
is switched.
This saves the state of the position and direction of the crewmate that
the text box position is based off of (if applicable), and the text
case of the text box, the script name of the script, and the original
(English) lines of the text box. I did not explicitly label the original
lines as English lines except in a main game context, because
technically, custom levels could have original lines in a different
language.
Unfortunately, this doesn't work for every text box in the game.
Notably, the Level Complete, Game Complete, number of crewmates
remaining, trinket collection, Intermission 1 guides, etc. text boxes
are special and require further fixes, but that will be coming in later
commits.
This is for consistency with all other functions dealing with the latest
created text box. There are several cases in custom levels where these
functions can be called even though there are no text boxes on screen.
It doesn't make sense to change the alignment of all existing text boxes
when you're not otherwise able to mutate the text. Whereas the point of
the 'all' argument in setfont is to be able to animate text boxes using
fonts.
There used to be a problem with the setfont and setrtl script commands.
Namely, if you used them in between text boxes naïvely, without any
careful thought, then the fading out text box would suddenly gain the
font of the new one. A kludge solution to this was implemented by simply
blocking the script until the existing text box faded out before
switching the font or RTL, and shipped for 2.4.0.
However, a better solution is to simply bake the font flags in to the
text box, so that way, if the level font switches, then the text box
keeps its font.
This is only for custom levels, because in the main game, the font in a
text box needs to be able to change depending on language. But it seems
like custom level translations weren't much on the roadmap, and so even
the existing hack didn't support changing the font based on translation
(even though translation of custom level cutscenes is supported). So
baking the font flags into the text box here doesn't make things any
worse.
It also makes things better, arguably, by allowing multiple text boxes
to exist on screen at once with different fonts.
Maybe in the future we'll need a flag that specifies that the font
should change depending on language if a translation in said language
exists for the text box, or something like that.
For people that want to override the fonts of every existing text box on
screen, you can specify "all" as the second parameter of setfont or
setrtl to do so.
This fixes a regression where attempting to warp to ship with a trinket
text box open in glitchrunner 2.0, and then incrementing the gamestate
afterwards, would result in a softlock. This is a speedrunning strat
that speedrunners use.
The state lock wasn't ever intended to remove any strats or anything,
just fix warping to ship under normal circumstances. So it's okay to
re-enable interrupting the state by having glitchrunner enabled.
This bug was reported by mohoc in the VVVVVV Speedrunning Discord
server.
Just like I changed the number één to be capitalized as Eén instead
of Één (due to this being a special case), I forgot to do the same
for the onelifemode.
This involves a couple of dialog boxes ("Congratulations!" and the
jukebox) which were supposed to have blank lines, but they weren't
in the translation.
Translators have been getting compensated for localization. Sometimes
they submit pull requests for their changes, but just because they do
doesn't mean that they won't get compensated.
The intent of this line was to make sure that any random contributor
wouldn't expect any compensation for their changes, so adding another
clause here still keeps that expectation while making it clear that it's
not like Terry _never_ compensates people. Even if, as a Swedish man
once sung, "Terry is a monster, I think we all know that".
LodePNG always loads PNG in big-endian RGBA format. For this reason,
when loading PNG files VVVVVV was specifying the format as ABGR and the
conversion would then be performed by SDL. However, then running on
big-endian machines, this conversion should not be performed at all, and
the surface format should then be set to RGBA.
We recently had a user come in the VVVVVV Discord not knowing that,
after running CMake, you then need to compile the game in a separate
step. This clarifies the instructions.
This fixes a segmentation fault caused by an out-of-bounds indexing
caused by an attempt to unwordwrap a string that starts with two
newlines.
The problem here is that in the branch of the function
string_unwordwrap() where `consecutive_newlines == 1`, the function does
not check that the string `result` isn't empty before attempting to
index `result.size()-1`. If `result` is empty, then `result.size()` is
0, and `result.size()-1` becomes -1, and indexing a string at position
-1 is always undefined behavior.
Funnily enough, a similar indexing happens just a few lines down, but
this time, there is a check to make sure that the string isn't empty
first. I'm unsure of how Dav999 forgot that check a few lines earlier.
This situation can happen in practice, with custom level localizations.
I made a level with a filename of testloc.vvvvvv and created a file at
lang/fr/levels/testloc/custom_cutscenes.xml with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<cutscenes>
<cutscene id="test" explanation="">
<dialogue speaker="cyan" english="This is text..." translation="blarg"/>
</cutscene>
</cutscenes>
Then I switched to French, created a script named `test`, and created a
text box that started with two newlines (so in total, the text box must
be at least 3 lines in length). Running the script triggers the segfault
when the text box is created. (Well, technically, on my machine, it
triggers an assertion fail in libstdc++ and aborts, but that's basically
the same thing.)
To fix this while still preserving the exact amount of newlines, if
`result` is empty, we add a newline instead of attempting to index the
string.
These strings had been replaced over time and the original versions
marked ***OUTDATED*** to allow for the original wordings to be reused
by the translators who had only translated the original ones.
(See lang/README-programmers.txt.)
Now, these strings have all been updated in every language, so it's
time to clean them up!
These were not in the English or any other language files. They should
be though, so that they can be translated and generally kept track of.
These aren't urgent either, since we have proxy strings that are used
if these are untranslated.
This hasn't been done since before we got some deliveries for 2.4,
so there are a few languages which added apostrophes as ' instead of
' in the XML (which is not wrong, but it gives diff noise whenever
there's a sync since VVVVVV writes them back as '...)
Also, we never synced "[Press {button} to toggle gameplay]" across
language files (now two strings with unfreeze/freeze), but that was
also a pretty last-minute string as far as I remember. Arabic did have
it because that language was added after the string was added, so it
got copied from English. I don't think this one is that urgent to
translate into every language for 2.4.1 since it's pretty well hidden
for most people, and it's surrounded by things that have to be English,
so it's as if it's supposed to be like that. Let's just include these
with whatever the next batch of strings is.
This is both easier for translators ("toggle" can be an annoying word)
and is useful in general because you can tell if gameplay is frozen
without having to have anything in the room that should normally be
moving but isn't.
I didn't follow the rule in lang/README-programmers.txt to keep the
original string around as ***OUTDATED*** in this case, since I know
only Arabic has it translated - we can just tell the Arabic translators
on Discord that this string was replaced.
It's kind of a bummer that L/R don't actually do anything... we should add ZL/ZR support at some point.
Also note that GameCube binds X to 'back' rather than B, this will be fixed by SDL_ActionSet for 2.5.
The original Chinese font (which we call "the Indienova font") was
received from the Chinese translators directly, and didn't come with
any license or copyright information other than that it was made by
Indienova. Questions have now been raised about the actual origin of
the characters in the font, and while we do have confirmation from the
translators that we're probably in the clear, they did suggest another
font for us to use, which we're switching to to be sure.
Some background information: the ideal font would probably be Ark Pixel
(https://github.com/TakWolf/ark-pixel-font/), but this font is not
finished yet. Therefore, the creators of Ark Pixel have made a font
that can be used as a placeholder to use in the meantime, Fusion Pixel
(https://github.com/TakWolf/fusion-pixel-font), which combines some
other fonts together in order to get full coverage. This is the font
we're now switching to.
It's not _that_ simple though - the ASCII part of Fusion Pixel is kinda
bad for us using it as a monospaced font. Normally I just replace the
ASCII set by the fullwidth characters, but in this font they were
almost entirely the same. So I instead picked the fullwidth characters
from Galmuri 12px, which is one of the "fusioned" fonts. Interestingly,
we happen to also use the 10px version of this as our Korean font, and
I like these Latin letters, so yay.
I also made the call to split the Chinese font into separate variants
for Simplified and Traditional Chinese. I was aware of the problem with
the Han Unification, but the Traditional Chinese translator said the
Indienova font also contains all the Traditional Chinese characters,
and they proofread the translation, so it was probably fine. Apparently
the difference between Simplified and Traditional Chinese variants of
the same characters are not that big, and it's acceptable. But
Fusion Pixel gives us separate versions of the font for Simplified and
Traditional Chinese, so this is a chance to get it right. Just kidding,
Fusion Pixel's Traditional variant switches out many characters that
were shared between Simplified and Traditional Chinese to Japanese
variants which are noticeably different. So it would be better to keep
using the SC font for TC, just like the Indienova font is SC only.
However: Ark Pixel does have a version with correct characters for
Traditional Chinese! So for the TC version of our font, I just took all
Chinese characters from the TC version of Ark Pixel where available.
That way, all characters I checked have changed to TC variants
correctly.
This removes every single translation of a wordy number that just
replaces it with the numeral.
This is because the documentation in README-translators.txt specifically
says
It's also possible to leave the translations for all the numbers
empty. In that case, numeric forms will always be used.
However, the translators for Japanese, Korean, and European Portuguese
clearly either didn't read this, or forgot to do so.
There is a very good reason to leave them alone if you want numerals;
namely that if you fill them in, you are prone to making errors. Like,
say, Japanese translating "Twelve" as "23", which is exactly what
happened. By blanking every translation, that error is fixed.
This reverts the following commits:
- 29f05c41b1
- f1bf1f683c
- a7b22919ae
- 2ed1aac67d
Recently, text images were changed to fade in with textboxes, where
before they previously appeared after the fade. This created a charming
effect where the images would appear to "load in" once the textbox
finishes fading in. This behavior really complements the retro
aesthetic the game is going for. Changing it to a fade is a needless
change in direction, as it was not a bug in the first place and looked
good already.
Additionally, custom levels have used text images (levelcomplete and
gamecomplete) in creative ways by replacing them with something else
to show as 'foreground' or as a cutscene image. Changing text images
to fade has unintended consequences for levels that have utilized
them in this fashion.
13d6b2d64c adds a check where you can't
type a pipe in script/terminal input fields anymore. It does this
completely incorrectly, checking if certain variables are set instead
of checking what's actually trying to be done.
This commit fixes that, simplifying the check a lot in the process.
This disables typing the pipe character in the data fields of terminals
and script boxes. Care has been taken to make sure that it's still
possible to type pipes in room text.
This is because pipes are the line separator in the big XML tag that
stores every single script line, and thus a script name with pipes would
end up being split up after the level file has been saved and loaded
again.
While a7b22919ae makes text sprites
modulate their RGB values, text images continued using alpha,
despite alpha blending not even being enabled, so the initial
commit didn't work right either.
This is quite a last-minute thing that was almost getting called off by
me discovering a critical segfault just now in testing this (whew) but
this shouldn't hurt.
This reverts commit a806b072bd.
It causes an instant segfault if there's no entities or if you're not
editing terminals/script boxes or something, whatever it's not
crucial as a last-minute fix.
No Death Mode is intended to be unlocked by getting at least S-rank in
at least 4 time trials. Before 2.3, completing a time trial put you at
the main menu, so you would always be notified of having unlocked No
Death Mode once you went to the play menu again. But since 2.3,
completing a time trial puts you back at the Time Trial selection
screen, which isn't the play menu, so you would need to back all the way
out first in order to get the notification. And since you don't actually
unlock No Death Mode until you see the notification, this would be
required to be able to play No Death Mode.
To fix this, I decided to do something a bit kludge-y and just re-use
the code to check and unlock No Death Mode when the player presses
ACTION on the Time Trial complete screen (and there's also another path
by pressing Escape). At least I put it in a function, so it's not a pure
copy-paste, although it might as well be. I don't have time to think of
a proper solution, but it would probably involve disentangling unlock
notifications from Menu::play, for starters. But that's for later.
This disables typing the pipe character in the data fields of terminals
and script boxes. Care has been taken to make sure that it's still
possible to type pipes in room text.
This is because pipes are the line separator in the big XML tag that
stores every single script line, and thus a script name with pipes would
end up being split up after the level file has been saved and loaded
again.
This makes it so that the boolean to draw mode indicator text is false
if there aren't any modes active.
Otherwise, when loading in, the in-game timer would only come in after a
few seconds instead of appearing when the fade-in finishes.
This fixes a bug where pressing Escape in the following menus would not
play Presenting VVVVVV (the title screen music) and would instead leave
you with silence: Game Complete, Time Trial complete, Game Over, and No
Death Mode complete.
This makes it so that only inputs between 1 and 255 inclusive will be
accepted. Otherwise, the command has no effect.
This is because the text case is stored as one byte in a string, and a
value of zero would be the null terminator.
We also want to minimize potential weirdness with integer wrapping if we
accept inputs from outside those bounds. While the textcase variable as
used throughout the codebase is plain unqualified `char` (which, unlike
other integers, exists in a quantum superposition of being signed and
unsigned depending on compiler, machine, and various other stuff) and so
there still might be issues there, we definitely don't want anything
higher than 255.
This adds the third_party/ directory to the list of paths that will
trigger a CI workflow when changed.
Not all updates to third-party dependencies will necessarily change
code, but we bump dependencies infrequently so there's not much of a
problem with triggering CI on every change to third_party/.
If the language is RTL, then the left and right menu navigation keys
should be reversed, because the menu layout goes from right to left.
This is to be consistent with the other menus in the game. The editor is
just a special case so it was overlooked.
There was an inconsistency where W and S couldn't be used in place of
the Up and Down arrow keys, but this has been fixed.
This only applies where W and S otherwise are not bound to anything
else. E.g. not the main editor (where W changes the warp direction and S
saves the level) and not the script editor (where W and S can be typed
inside a script), but the script list is fine.
Currently, it's a bit jarring that text box overlays (which are text box
images, e.g. Level Complete and Game Complete, and sprites, e.g. the
crewmates) will suddenly appear when their text box has fully faded in
and suddenly disappear once it starts fading out.
This makes it so that text box overlays will be faded in and out
smoothly along with the rest of the text box fading in and out.
Transparent text boxes are not affected, as they do not fade in and out
at all. Thus, text box overlays in transparent text boxes will still
suddenly appear and disappear as usual.
The number "one" in Dutch is "één" (silly, I know :P). Capital letters
can have accents, but there's an exception where for this specific
word, the first accent is much more often left off than not. So I'm
now using wordy2 as the uppercase variants of all the numbers, and
using that instead of the |upper flag.
I forgot that the position of the activity zone can vary based on
setactivityposition() in custom levels. So account for that.
Additionally, if the activity zone prompt is far down enough, then we
don't need to move the mode text at all.
Dav999 notified me that if multiple screenshots are taken in the same
second, the second screenshot has `_2` appended to it and so on. We do
the same here by storing the current timestamp and a counter.
This doesn't prevent overwriting files if you have system time that
changes, or have multiple instances of VVVVVV running at the same time,
but my position on those cases is as follows: Don't do that.
This makes the mode indicator text be visible even if there is an
activity zone prompt on screen, by making it so that it gets moved if an
activity prompt is being rendered.
This is to make sure that it's visible no matter what, even if e.g. a
custom level starts the player on an activity zone.
Trophy text can overlap with the timer. How bad it is depends on the
localization but in English some text definitely overlaps.
Simple fix is to disable rendering the timer if we are rendering any
trophy text.
These are functions used in other files that are not on the
GraphicsResources class but are implemented inside the
GraphicsResources.cpp file. For some reason they were never put in the
GraphicsResources.h file until now, even though it's a perfectly good
header file to put them in.
Filenames are timestamped now, down to the second. If you take multiple
screenshots in the same second, then the last one will overwrite the
others. This seems to be how other screenshot programs operate so I
don't think it matters if you can't take more than one per second.
Additionally, 1x screenshots (320x240) will go in the 1x/ subdirectory,
and 2x screenshots (640x480) will go in the 2x/ subdirectory.
Originally, I was thinking of adding a notification text that you took a
screenshot, but this is better because it is language-agnostic and it
doesn't contribute to potential UI clutter/clashing.
It flashes yellow if the screenshot successfully saved, and red if it
didn't.
The plan is to have Steam screenshots always be 2x, but in the VVVVVV
screenshots directory (for F6 keybind) save both 1x and 2x.
Again, just for now, the 2x screenshot is being saved to a temporary
location for testing and will get proper timestamps later.
One problem with internal screenshot capture is that we rely on SDL's
render subsystem to flip the screen in Flip Mode, while leaving our
actual screen untouched. Since we source the screenshot from the screen
and not what SDL renders, we need to flip the screenshot ourselves when
saving an internal capture.
To do this, we need to support 24-bit colors in DrawPixel() and
ReadPixel(). Luckily, this isn't too hard to do. A 24-bit color is just
a tuple of three bytes, and we just need to do a small amount of bitwise
math to pack/unpack them to a single integer for SDL_GetRGB() and
SDL_MapRGB().
Using the Steamworks API, we can hook the screenshot function and listen
for a screenshot request callback to send in our own screenshot. This
applies the screenshot improvements to Steam screenshots as well.
Doing this requires adding some C wrapper functions, as our interface
with the Steam API is only conducted through C.
"But people already have screenshot tools", you might protest. The
rationale is simple: If you play with any video setting other than 1x
windowed (no stretching and no letterbox), then your screenshot will be
too big if you want the internal resolution of 320x240, and downscaling
will be an inconvenience.
The point is to make screenshots based off of internal resolution so
they are always pixel perfect and ideally never have to be altered once
taken.
I've added the keybind of F6 to do this.
Right now it saves to a temporary test location with the same filename;
future commits will save to properly-timestamped filenames.
This is mostly so people making levels in an RTL language have a more
pleasant and logical experience. If roomtext is placed in a level set
to RTL, it will get p1=1, which makes that roomtext right-aligned.
Because, imagine for English you click to place roomtext, and the text
runs left of where you clicked, which wouldn't be logical.
Since it's an entity-bound property, switching RTL on and off either in
the editor or via a script does not affect existing entities.
This remaps the keybind to reload language files from F12 to F8.
This is because the F12 keybind conflicts with the default Steam keybind
to take a Steam screenshot.
I chose F8 because it is next to another keybind that reloads stuff, F9
(which reloads assets in the editor).
Fixes#1089.
While working on adding a screenshots keybind, I encountered a link
error with these functions. Wrapping them in `extern "C"` fixed it. It's
most likely due to the fact that they were `extern "C"` in the header,
but not in the `.cpp` file. They should be both `extern "C"`'d
regardless.
If you load in to gameplay with invincibility mode, glitchrunner mode,
Flip Mode, or slowdown enabled, then there will be text displayed on
screen for a few seconds that says so.
This is to serve as a useful reminder. A common pitfall with using
invincibility is forgetting to turn it off when you don't want it
anymore. What usually happens is that players forget that they have it
on until they encounter a hazard. Now, they can realize it as soon as
they load in.
See #1091.
If text is set to be centered, but is so long that it starts running
offscreen on both sides, the print function instead makes the text
start no further left than the left border of the screen (x=0).
This is because text running offscreen at the end only is more readable
and looks less sloppy than running offscreen at both sides.
For RTL, the opposite applies, so it now also works oppositely for RTL
prints, where centered strings will only run offscreen on the left side
of the screen.
Spaces on the left and right would end up on the other side in RTL,
which made the "You have rescued a crewmate!" text overlap with the
crewmate sprite, and makes the [C[C[C[C[Captain!] dialogs have spaces
on the left instead of on the right. So, best thing is to just swap
the directions so that they match.
They're invisible in font::print(), but they were still considered
characters with widths in the width function. This change made the
levels screen look better in RTL too - I was wondering why the level
options were too far left.
If you copy-paste a newline character where it's not interpreted, such
as in a level title, the print function wouldn't treat it any special.
font::print_wrap() would, but that's not used here.
However, now that bidi is involved, the newline is passed straight to
SheenBidi which interprets it as a new line (which would need a new
SBLine to be created, or maybe even a new SBParagraph if there's two).
All while we're still treating it as a single line. This means the text
would just stop being displayed after the first newline. This is now
fixed by treating all newlines as spaces.
This has a lot of reading-orientation stuff on it like "Key: value",
so easiest is to just flip the whole design of the screen rather than
trying to flip individual strings.
I forgot to add the PR_RTL_XFLIP flag to these menu options, so they
were always left-aligned, no matter what.
What actually took me a bit to figure out was how to make the level
completion stars work regardless of the contents of the title - the
stars should always be to the left of the title in an LTR language, and
always to the right of the title in an RTL language. Level titles can
contain bidi characters regardless of the level's rtl flag being set,
so I just let bidi handle all the level menu options, with some control
characters to make sure everything always appears in the correct order.
Stuff like centertext="1" and padtowidth="264" in cutscene translations
looked wrong in RTL mode, both with Arabic and English text. For Arabic
text, I could easily fix the problem by not counting the number of
codepoints (and assuming they all have the same glyph width), but by
instead taking the width of the string as reported for the font, and
dividing it by the glyph width. This leaves English text still looking
weird in RTL mode. But this shouldn't be a problem either: the Arabic
translations will probably be in Arabic (where the problem doesn't
happen), and I can get English text to show up fine by wrapping it in
U+2066 LEFT-TO-RIGHT ISOLATE and U+2069 POP DIRECTIONAL ISOLATE. So it
looks like an inherent quirk of bidi, that translators familiar with
bidi can easily grasp and fix.
This is main-game only functionality, so it shouldn't break existing
custom levels. We should just make sure textboxes in other languages
aren't broken, but from my testing, it's completely fine - in fact, it
should've improved if it was broken.
Instead of just up/down, you can also control menus with left/right.
Which is illogical in Arabic... No big deal, I imagined this code
to become much worse than it did. (And action sets is probably gonna
refactor the whole thing anyway)
Okay, the "Font:" thing needed some local code after all, because both
the interface font as well as the level font are used there. But it's
good enough - all the other places can just use the flag.
Notably, I also used this for the menus, since the existing ones are
kinda LTR-oriented, and it's something that we don't *really* have to
do, but I think it shows we care!
This lets you mirror the X axis specifically in RTL languages, so the
left border is 320 and the right border is 0, and invert the meaning of
PR_LEFT (0) and PR_RIGHT. Most of the time this is not necessary,
it's just for stuff where a label is followed by a different print,
like "Font: " followed by the font name, time trial time displays, etc
With the <font> tag (which doesn't indicate RTL-ness as explained),
we've had a setfont(font) scripting command. Now we have an <rtl>
tag, so we need a setrtl(on/off) command too to control that.
Again, the RTL property controls whether textboxes will be
right-aligned, and that kind of stuff. It can't be font-bound, since
Space Station supports Hebrew characters and we want to be able to
support, say, a Hebrew translation or Hebrew levels in the future
without having to make a dedicated (or duplicated) font for it.
Therefore it's a property of both the language pack as well as custom
levels - like custom levels already had a <font> tag, they now also
have an <rtl> tag that sets this property.
Right now, we'll have to hardcode it so the menu option for the Arabic
font sets the <rtl> property to 1, and all the other options set it to
0. But it's future-proof in that we can later decide to split the
option for Space Station into an LTR option and an RTL option (so both
"english/..." and "עברית" would select Space Station, but one sets the
RTL property to 0 and the other sets it to 1).
This now returns true if any of the characters in the text belong to
the Arabic or Hebrew alphabet, or are one of the Unicode directional
formatting characters. This is just so the bidi machinery doesn't have
to run 100% of the time for 100% of the languages. I will also make it
so the Arabic language pack, as well as custom levels, have an RTL
attribute that always enables bidi (and does things like
right-alignment in textboxes and other design-flipping)
I'm now using SheenBidi to reorder RTL and bidirectional text properly
at text rendering time! For Arabic this is still missing reshaping, but
everything's looking really promising now!
The code changes are really non-invasive. The changes to Font.cpp are
absolutely minimal:
1305+ if (bidi_should_transform(text))
1306+ {
1307+ text = bidi_transform(text);
1308+ }
There's now a FontBidi.cpp, which implements these two functions,
notably bidi_transform(), which takes a UTF-8 encoded string and
returns another UTF-8 encoded string that has bidi reorderings and
reshapings applied.
In that function, SheenBidi gives us information about where in the
input string runs start and end, and on a basic level, all we need to
do there is to concatenate the parts together in the order that we're
given them, and to reverse the RTL runs (recognizable by odd levels).
As this is a proof-of-concept, bidi_should_transform() still always
returns true, applying the bidi algorithm to all languages and all
strings. I'm thinking of enabling bidi only when the language/font
metadata enables RTL (which could be for the interface or for a custom
level), or outside of that, at least when RTL characters are detected
(such as Arabic or Hebrew Unicode blocks).
I'm going to give it a shot to use this for bidi text support, it looks
like it's a pretty lightweight, compatible and low-dependency library
which is definitely a plus. We'll still need to do reshaping ourselves,
but that's the easy part compared to bidi.
This makes it so that the main CI workflow will only trigger if a change
is made to a code file in desktop_version/ (as the CI is only for
desktop_version/), or if the CI file itself is changed.
The CI workflow for Android will only trigger if Android-specific code
_could have_ changed. This includes all code that is definitely
Android-specific (e.g. Java files), but also C/C++ files that have
__ANDROID__ ifdefs.
Unfortunately, it's not possible to reuse the same list of paths across
two different event trigger types[1]. So we have to copy-paste here.
[1]: https://github.com/orgs/community/discussions/37645
I had added 1px spaces in some Japanese strings with buttons in them,
to avoid the button glyphs touching the rest of the text. However, the
Japanese translator later ended up putting full spaces in, not noticing
the hair spaces. So now the space was 1 pixel wider than it should've
been, and it's better to remove them.
This fixes a bug where you could still drag an entity around with the
debugger inactive if you were holding the entity while disabling the
debugger with Y. Furthermore, you couldn't even drop the entity even if
you wanted to.
There is a clash between the timer text and the "Survive for 60
seconds!" text. It's minor in English but it can be worse in other
languages (e.g. Polish).
So make the timer go away when that text is onscreen.
The "[Press {button} to return to editor]" and the "TIME:" text
overlapped, which resulted in an ugly clash.
To fix this, make the return editor text take priority over the timer
text. This involves a minor refactor to first calculate whether or not
we should draw the return editor text before we check if we should draw
the timer text.
Some languages have different spellings of wordy numbers based on the
gender of the things they're counting (uno crewmate versus una trinket)
or what a number's role is in the sentence (e.g. twenta out of twentu).
We've always had the idea we couldn't support such complex differences
though, because the game can't be adapted to know what gender each
object will have and what word classes might exist in other languages,
so translators would in those cases just have to forgo the wordy
numbers and just let the game use "20 out of 20".
A solution we came up semi-recently though (after all translations were
finished except for Arabic), was to allow the translator to define
however many classes of wordy numbers they need, and fill them all out.
This would not need the game to be *adapted* for every language's
specific grammar and word genders/classes. Instead, the translator
would just choose their correct self-defined class at the time they use
`wordy` in the VFormat placeholder. Something like
{n|wordy|class=feminine}, or {n|wordy_feminine}.
So this would benefit several languages, but we came up with the
solution a little late for all languages to benefit from it. The Arabic
translators asked for two separate classes of wordy numbers though, so
my plan is to first just have a second list of wordy numbers
(translation2 in numbers.xml), which can be accessed by passing the
`wordy2` flag to VFormat, instead of `wordy`.
Once 2.4 is released, we can take our time to do it properly. This
would involve the ability for translators to define however many
classes they need, to name them what they want, and this name would
then be useable in VFormat placeholders. We can convert all existing
translations to have one class defined by default, such as "wordy", or
"translation" depending on implementation, but there's not so much
concern for maintaining backwards compatibility here, so we can do a
mass-switchover for all language files. That said, it wouldn't be too
hard to add a special case for "translation" being "wordy" either.
We can then ask translators if they would like to change anything with
the new system in place.
For now, we can use this system for Arabic, maybe Spanish since there
were complaints about uno/una, and *maybe* Dutch (it has a thing where
the number "one" is often capitalized differently, but it's not
mandatory per se)
For some reason, the default behavior of SDL and/or Windows(?) (I only
tested this on Windows) seems to result in the fact that if any SDL app
doesn't account for it, there is no way for Japanese and Chinese
speakers to know what they're typing in.
How IMEs are supposed to work is that you can type words as sort of
WIP versions, and then select out of a list of candidates what the
final result should be. The app may display the WIP text and tell the
IME where the text field is so that the IME's menu can be displayed
around it. But if the app doesn't say where the text field is, then the
candidate list can also be displayed at the corner of the screen, which
is done in Minecraft.
By default, however, SDL apps don't get a candidate list at all, which
means you're basically flying blind as to what you're typing in, and
you would have to basically open notepad and copy-paste everything from
there - unless I'm missing something.
This commit sets the SDL_HINT_IME_SHOW_UI hint (added in SDL 2.0.18
apparently), so that the candidate list is at least shown in the corner.
We can probably deal with positioning and uncommitted text later.
The TAB bind is used in both roomname translator mode and the level
debugger. To fix this, the TAB keybind will prioritize roomname
translator mode, unless the debugger text is enabled (with the Y
keybind), in which case the debugger takes priority. Additionally, the
roomname translator text will not render when the debugger text is
shown.
Fixes#1094.
- Added a logo
- Cleaned up the description, answering the number one question I get asked about the source code
- Added link to the discord to direct potential contributors to
- Updated credits
- fixed some links in the credits (Magnus' site is currently down, but this is temporary)
This ensures that the game won't silently fail to start if it can't
initialize the filesystem. Instead, it will fail loudly by popping open
a message box (using SDL_ShowSimpleMessageBox).
The motivation for this comes from issue #1010 where this is likely to
occur if the user has Controlled Folder Access enabled on Windows, but I
didn't want to put in the work to specifically detect CFA (and not sure
if it's even possible if it turns out that the OS just gives a standard
"permission denied" in this case). At least any message box is better
than silently failing but printing to console when most users don't know
what a console is.
Fixes#1010.
This is the same as commit 70357a65bf
("Fix regression: Warp BG lerps in reverse direction"), but for the
tower background.
This bug is most visible when moving the camera in a tower using
invincibility, or holding down ACTION during the credits scroll.
This lets you hold down F to fast-forward the game if you have the level
debugger interface open (with Y) and the game isn't paused.
This is most useful for quickly skipping through cutscenes to test
something.
This code was introduced by Dav999 in
abf12632bb (PR #1077), but it contains a
memory error. I spotted this with Valgrind.
The problem comes from the fact that `max_codepoint` is calculated from
the width and height of the surface (which will have the same width and
height as the source `font.png` from the filesystem). Let's work through
an example using a typical 128 by 128 `font.png` and an 8 by 8 glyph.
`chars_per_line` is calculated by dividing the width of the image
(`temp_surface->w`, or 128) by `f->glyph_w` (8), yielding 16.
`max_codepoint` is calculated by first calculating the height of the
image divided by the height of the glyph - which here just happens to be
the same as `chars_per_line` (16) since we have a square `font.png` -
and then multiplying the result by `chars_per_line`. 16 times 16 is 256.
Now it is important to recognize here that this is the _amount_ of
glyphs in `font.png`. It is _not_ the last codepoint in the image. To
see why, consider the fact that codepoint 0 is contained in the image.
If we have codepoint 0, then we can't have codepoint 256, because that
would imply that we have 257 codepoints, but clearly, we don't. If we
try to read codepoint 256, then after working through the calculations
to read the glyphs, we would be trying to read from pixel columns 0
through 7 and pixel rows 128 through 135... in a 128 by 128 image...
which is clearly incorrect.
Therefore, it's incorrect to write the upper bound of the for-loop
iterating over every codepoint as `codepoint <= max_codepoint` instead
of `codepoint < max_codepoint`.
I was running the game through Valgrind and I noticed a memory error
where the game was attempting to read a pixel that was just outside the
image. Since this is an error that doesn't immediately result in a
segfault, I figured that it would be prudent to put in an assertion to
make it loud and clear that a memory error is, in fact, happening here.
Similarly, drawing to a pixel just outside the surface wouldn't result
in a crash, so I copy-pasted the check there too (with changes).
If you're in (5, 5) (1-indexed) and you resize the map to (4,5), the
editor stays in (5, 5). This has no real consequences, other than
possibly confusing the user, but it should probably be fixed anyway.
Turns out the string I fixed in the previous commit was also never
noticed in German. For that one, I simply used the wording that was
used in the old hardcoded-ACTION string (with my German knowledge,
I'm confident that's still correct).
I just discovered this: whereas 2.3 and older versions make gravity
and warp lines - when placed in the editor - stick out one tile
offscreen, the latest version stops the lines at the room border.
This change restores the old behavior, and it's a simple fix: the
refactored code was written to let tiles outside the room block
gravity/warp lines. Instead of all offscreen tiles blocking lines, now
there's a 1-tile padding around the room that will let them through.
The new localization-related credits are placed 5 characters from
the left border in the rolling credits (at x=40), which means the
limit was 35 8x8 characters. Which was broken by several languages.
So instead, move the string leftward a bit if it would run offscreen
otherwise.
If you go into the middle of the list of translators in the main menu
credits, then press Escape, and then go into the credits again, the
first page of the list may start at the wrong place, because while
game.translator_credits_pagenum was reset to 0,
game.current_credits_list_index wasn't. This is fixed now.
The header "Translators", as well as the language names, were using
PR_FONT_8X8, even though it was translatable text. This is now fixed.
(Also, the CJK spacing for the language names is now higher because
that looked nicer)
VVVVVV 2.2 only supported displaying characters 00-7F with its font
system. VVVVVV 2.3 added support for unicode, by supplying a font.txt
with all the characters that are in the font image. But 2.3 made
another change that I didn't immediately realize, even after reading
the code: if font.txt is not present, then the font is not assumed to
have _only_ 00-7F, but _all_ of unicode, as far as the image dimensions
allow.
However, an inconsistency I _did_ notice is how unknown characters
would be rendered in 2.3. If a font had a font.txt, then any unknown
characters would be shown as a '?'. If a font had no font.txt however,
then suddenly any unknown characters would just come out as a space.
I fixed this behavior with the new font system; but what was actually
happening for characters to come out blank is that characters up to
U+00FF, which _were_ technically in the font image but as fully
transparent, would be shown as they were in the image, and characters
beyond U+00FF wouldn't be shown since they were outside of the image.
I don't really want to show blank characters for any character between
80-FF if it is technically inside the image, because pretty much every
single ASCII-only font.png in existence (including the one in data.zip)
contains a blank lower half, just because the font in the game had
always had this specific resolution. (We didn't want to do things that
might crash the game because something was different from what it
expected...)
We have had some confusing occasions before with the old behavior where
the fonts weren't correctly packaged or something (like when the
Catalan translator was sent the first version of the translator pack,
or when people customize their fonts wrong) and special characters were
just blank spaces.
So, instead, for characters beyond 7F, I decided to consider them part
of the font, as long as they are not blank. That means, if a character
beyond the ASCII range has any (non-alpha-0) pixels, then it will be
added, otherwise it won't be. This is just to handle legacy fonts, and
the case where all fonts are missing and the one from data.zip is used;
new fonts should just use .fontmeta or .txt to define their characters.
The top of the programmers readme now says that you need the
translators readme to translate the game into a new language. Also,
since language file syncing now works to populate an empty language
folder, document that in the translators readme as well.
Two translators thus far have tried to populate initial language files
by creating a blank folder and then using the in-game sync option. For
example, see #1078.
That is not how the sync option was intended to be used, but it's
really close to getting everything, so I decided to just complete the
support by making sure numbers.xml is copied from English, and making
sure meta.xml is filled in with English text and not text from an
arbitrary language. Also, minor detail on plural form 1 being set to 1
by default if reset, so strings_plural.xml is fully consistent too.
Or well, lock yourself out if you don't have (easy) access to a
keyboard, like on Steam Deck.
In 2.3, this problem used to be much worse, since you could bind any
button to "menu" - which is actually also "return" in menus - and that
button could then no longer be bound to any other action, because
exiting the bindings menu had priority over assigning a different
binding. The result would be that people could have all their buttons
bound to "escape" with no way of undoing it or using their controllers
at all other than manually going into their config file to change it.
In 2.4, the most important bugs in the bindings menu are fixed, but
it's still possible to remove all your bindings from the "flip"
(confirm) action, meaning you can't navigate the menus anymore with a
controller to fix your bindings or even do anything.
There is one interesting part to all this: if an action has no buttons
bound to it at all when the game is started, then that action is
populated with the default button for that action. This is done for
each action separately, without accounting for the case where the
default button was already bound to another action which was not empty.
(This is something that the binding menu does try to prevent).
Therefore, having no buttons bound to "flip" while having A and B bound
to "menu", would result in A being bound to "flip" and A and B bound to
"menu".
That would still make you unable to enter the gamepad menu, since both
"confirm" and "return" are pressed in a row.
This commit fixes the specific situation where flip/confirm buttons are
also bound to menu/return, by removing all buttons that are in the flip
button list from the menu list. This means that, on Steam Deck, you can
still go to your bindings menu.
Seems like I made a mistake while originally writing the "make
autotiling base" code. This commit fixes the warp background turning
into solid tiles when you switch to a different tileset.
might consider adding an Español (latam) edit next year, but this is
enough for 2.4. We're using "Español (es)" instead of "Castellano"
because our translator prefers it
This commit adds translation credits to the game's end credits
screen. Note that this is not implemented into the menu credits
screen yet. The translator name list is subject to tweaks, and
additionally some localised strings ("Localisation Project Led by"
and "Pan-European Font Design by") run off the screen in some
languages (Catalan, Spanish, Irish, Italian, Dutch, European
Portuguese and Ukrainian) and will need to be addressed later.
I put a main focus on the first cutscenes in the game, changing the
first "Uh oh..." from something like "Oh dear..." to "Oh no..." to make
sure it always sounds right. (The real translation of "Uh oh" is "O-o",
but that seemed too easy to read wrong for the first line in the game
that I wanted to avoid it altogether.)
Textboxes created with graphics.createtextboxflipme() use PR_FONT_LEVEL
by default, but can be overridden with graphics.textboxprintflags() to,
for example, set PR_FONT_INTERFACE. This happens for the textboxes on
the Game Complete screen, which use interface text. The textboxes are
centered by setting the X position to -1 though, which means they're
solely centered based on the width of the first line, in the level
font (because the font hasn't been changed to the interface font yet).
Normally, this isn't a problem, because in the main game (where the
Game Complete screen usually appears), the level font is always equal
to the interface font. However, in custom levels you can still get it
(by calling gamestate 3500) and in that case some of the text may be
misaligned. This change fixes that by adding graphics.textboxcenterx()
to these textboxes.
As far as I can tell, these are the only textboxes that are centered
by just x=-1 despite changing the font afterwards.
If you had a pink space station background, and switched to a different
tileset, some solid tiles would be placed instead. This commit fixes
that by transforming the room into the basic autotiling tiles before
changing the tileset itself. The reason why I chose this solution is
because it will help with a future change, being unhardcoding warp zone
backgrounds (which'll help with custom autotiling, if that becomes a
thing.)
Now that the language files are fairly stable, we should be able to do
this without any accidental reverts taking place (if any do happen, it
should be easy to see and prevent)
With the recent change to drawing overlays (images and sprites) from
PR #1058, it's starting to get a bit hairy. This names the conditionals
responsible for determining if the text box is transparent (checking
that all of its RGB is 0) and if overlays should be drawn or not (which
is now either when it's opaque or transparent).
Textsprites and textimages no longer wait for the opacity
value in order to display within transparent textboxes.
Text sprites in normal opaque textboxes are not affected
by this change.
Fixes#1057.
Based on Ethan's hunch, I simply removed the format comparison that
decides whether to halt and restart, or reuse the voice. Voices are
now always restarted when playing a new track.
This also simplifies the code somewhat: `MusicTrack::musicVoiceFormat`
was now no longer used, and an `if (!IsHalted())` was no longer
necessary because `Halt()` already does that. So those are now removed
as well.
This string is used both in time trials (alongside "MORTS :" and
"BLINGS :") as well as outside time trials if you enable the in-game
timer. In English, this looks like "TIME:1:23.45". Since French adds
a space before the colon, it will look like "TEMPS :1:23.45" instead.
Therefore, I've added a space after the colon as well.
At first my CJK changes also misaligned this sprite, and my solution
that time was to position the textbox higher depending on the height
of the textbox, so it would be centered around the crewmate sprite
(which stayed at a hardcoded place onscreen). Recently, #987 changed
these sprites to be relative to the position of the textbox instead of
relative to the screen, which is much more logical, but it stopped
centering these sprites again. But it's an easy fix: simply account for
the extra-added height when adding the sprite in.
This hasn't been relevant for years now. Even in 2.3, this wasn't
relevant, but we added a disclaimer saying that it only applies to 2.2.
But now issue #1052 has been opened specifically pointing to this
section as something that should be removed. Therefore, I'm removing it.
This fixes a regression caused by PR #923 (the PR that moved rendering
to be GPU-based) where the interpolation of the horizontal and vertical
warp backgrounds (in over-30-FPS mode) was in the wrong direction, which
makes them look blurry.
This happens because the arguments to the `lerp` function were in the
wrong, reverse order.
On the VVVVVV Discord server, Ally raised the argument that they were in
the same order before she made the changes; therefore the previous code
was also incorrect and it wasn't her fault. However, this argument is
incorrect, because in that case, the reverse order _is_ the correct
order.
The reason that it's now the wrong order is because the output of `lerp`
is now being used as the argument to a source rectangle. Previously, the
output of `lerp` was being used as the offset argument to
`ScrollSurface`, which is analogous to being a destination rectangle.
Fixes#1038.
The main issue was mostly that we have to build C files as C++ in some
cases, and extern "C" wasn't being used everywhere, so linker errors
popped up. The rest is the usual tedious VS2010 stuff like casting void*
to other stuff, so this commit as a whole is pretty boring!
*subject to changes
Also, Traditional Chinese is current using the Simplified Chinese graphics, which is acceptable but not ideal:
Obey -> 服從 (ok to use simplified 服从)
Lies -> 謊言 (ok to use simplified 谎言)
The other words are the same for Simplified Chinese and Traditional Chinese.
commit 3d6802add8
Author: Dav999 <dav999.tolp@gmail.com>
Date: Thu Oct 19 17:16:01 2023 +0200
Change AVOID to FAINIC in Irish
commit 21fd84f479
Author: Dav999 <dav999.tolp@gmail.com>
Date: Thu Oct 19 17:04:27 2023 +0200
Partial final strings for Esperanto
This does not yet include the new localization credits, but I already
had all the other strings.
commit 45382a358c
Author: Dav999 <dav999.tolp@gmail.com>
Date: Thu Oct 19 17:01:30 2023 +0200
Final strings for Dutch
I also decided to change AVOID from ONTWIJKEN to ONTWIJK, to make it
a bit more fitting as if it's an actual word enemy with length
restrictions, heh. (Not that it's an abbreviation - it's just an
imperative instead of an infinitive. And those terms I had to look up)
This commit adds new debug lines while you're NOT hovering over an
entity or a block. Additionally, coordinates are now displayed smaller,
to not take up as much vertical space.
The level debugger is toggleable in playtesting mode by pressing Y.
You can toggle whether or not the game is paused inside of the debugger
by pressing TAB. The debugger screen allows you to see entity and block
properties, and allows you to move them around.
The hardest room used to be stored as a room name in whatever language
it was in when you last died enough times to break the record (before
localization, that was always English). Even after localization became
a thing we could get away with this since we only had a single font,
but now we might have actual question marks appearing when the new font
doesn't support characters from the old language.
Therefore, this commit adds more info about the hardest room to save
files - everything that is needed to know in order to do the
translation at display time. These are hardestroom_x and hardestroom_y
for the room coordinates, as well as hardestroom_specialname to mark
special names, in addition to changing the stored room name back to
English. I've also added hardestroom_finalstretch in case we later
decide to drop the English name as a key and rely on just the
coordinates (even though I think that change itself would be more
complicated than any simplification it would accomplish, and I don't
think it's necessary, but better to have it if we do need it later)
As described in #1016, there used to be a bug that inflated
levelstats.vvv in 2.3, which was fixed in 2.4, but there was no way
for inflated files to get smaller yet.
This commit changes the storage of levelstats from a std::vector of
structs to a std::map, so that uniqueness is guaranteed and thus the
stats can be optimized automatically. And it also simplifies *and*
optimizes the code that handles the levelstats - no more big loops that
iterated over every element to find the matching level.
(Farewell to the "life optimisation and all that" comment, too)
I tested this with both my own levelstats.vvv, as well as some inflated
ones (including Balneor's 93 MB one) and saw this code correctly reduce
the filesize and speed up the levels list.
Fixes#1016.
The declarations of `std::vector<std::string> customlevelnames` and
`std::vector<int> customlevelscores` are made quite early in the
function, commented with "Old system", but the place where the old
system is processed is after a big chunk of code that processes the new
system (and indeed never uses these vectors). So for readability,
they're now closer to where they're used.
`levelcomplete` and `gamecomplete` were hardcoded using textbox colors
which were offset by 1. This PR fixes that, no longer requiring
slightly-off colors, and instead adding a new property to textboxes
which tell the game to display either level complete or game complete.
This commit adds a system for displaying sprites in textboxes, meant to
replace the hardcoded system in the main game. This does not support
levelcomplete.png and gamecomplete.png yet, which will most likely just
be special cases.
This ensures loading a 2.4 save in the English-only 2.3 or earlier
doesn't result in missing characters because a translated area name
appears in the save file. We are not reading from <summary> anymore
in 2.4.
The way this is done is by not translating the area names inside
mapclass::currentarea(), but at the callsites other than the one which
saves the <summary>.
For both `tele` and `quick`, I removed these attributes of class Game:
- std::string *_gametime
- int *_trinkets
- std::string *_currentarea
- bool *_crewstats[numcrew]
All this info can now be gotten from members of Game::last_telesave and
Game::last_telesave. I've also cleaned up the continue menu to not have
all the display code appear twice (once for telesave and once for
quicksave).
RIP "Error! Error!" though lol
This is what got saved to the area part of the <summary> tags, and it
was specifically set upon pressing ACTION to save in the map menu.
Which meant tsave.vvv may not get an accurate area name (notably
"nowhere" if you hadn't quicksaved before in that session) even though
it's not displayed anywhere so it didn't really matter. But this
variable can be removed - there's only one place where <summary> is
written for both quicksaves and telesaves, so that now gets the area
at saving time.
Fun fact: custom level quicksaves also have a <summary> tag, and it's
even less functional than the one in tsave.vvv, because it stores
whatever main-game area name applies to your current coordinates.
So I simply filled in the level's name instead (just like what the
actual save box says).
Game::telesummary and Game::quicksummary stored the summary string for
the save files - which is the <summary> tag that says something like
"Space Station, 10:30:59". The game only ever displays the quicksave
variant of these two, for "Last Save:" on the map menu's SAVE tab.
So the telesave has a <summary> too, but it's never displayed anywhere.
(In fact, the area is often set to "nowhere"...)
However, the summary strings have another function: detect that both
the telesave and quicksave exist. If a summary string for a save is
empty, then that save is considered not to exist.
I'm refactoring the summary string system, by making the new variables
Game::last_telesave and Game::last_quicksave of type struct
Game::Summary. This struct should have all data necessary to display
the summary string at runtime, and thus translate it at runtime (so
we don't store a summary in a certain language and then display it in
the wrong font later - the summary can always be in the current
language). It also has an `exists` member, to replace the need to
check for empty strings.
The <summary> tag is now completely unused, but is still written to
for older versions of the game to read.
(This commit does not add the new string to the language files, since
Terry now added it separately in his own branch)
It used to take a single int: the area number returned by
mapclass::area(roomx, roomy). All uses of currentarea() were called
with an extra area() call as its argument. Additionally, there's a
good reason why currentarea() should have the room coordinates: in one
of the cases that it's called, there's a special case for the ship's
coordinates. This results in the SAVE screen in the map menu being able
to show "The Ship", while the continue screen shows "Dimension VVVVVV"
instead. Therefore, why not put that exception inside currentarea()
instead, and remove a few callsite map.area() wrappers by making
currentarea() take the room x and y coordinates?
Since #1047 was merged, we now make the user build the SDL prefab
themselves (as SDL does not publish Maven packages yet). Here are some
instructions for doing that.
Whenever I'd compile on Windows, I'd see the literal text "%cs" in the
main menu instead of the commit date. I never thought much of it (at
least it runs, and the date only shows up in development builds). Now
that I've also seen a screenshot from Terry with it, I decided to look
into it further. Looks like it's a format string that our gits on
Windows aren't recognizing for whatever reason - probably because
they're too old. I have git version 2.23.0.windows.1, and checking its
help page for `git log`, under PRETTY FORMATS, %cs is missing as an
option, while some other options are still there. So the option was
probably added sometime between that version and 2.34.1, which is the
one I have on Linux, where %cs does work.
Luckily, %cd with --date=short seems equivalent, and better supported,
so we can just use that instead.
Recent versions of CMake emit the following:
CMake Deprecation Warning at CMakeLists.txt:4 (cmake_minimum_required):
Compatibility with CMake < 3.5 will be removed from a future version of
CMake.
Update the VERSION argument <min> value or use a ...<max> suffix to tell
CMake that the project does not need compatibility with older versions.
Reading the documentation further, adding a max refers to the max
version compatibility of CMake _policies_. Adding a max of 3.5 makes the
warning go away, so it seems that the warning is more about policies
than anything else.
This will still work on 2.8.12 as the extra dots will be seen as a
version component separator, ignoring the max version.
Language folders can now have a graphics folder, with these files:
- sprites.png and flipsprites.png: spritesheets which contain
translated versions of the word enemies and checkpoints
- spritesmask.xml: an XML file containing all the sprites that should
be copied from the translated sprites and flipsprites images to
the original sprites/flipsprites.
This means that the translated spritesheets don't have to contain ALL
sprites - they only have to contain the translated ones. When loading
them, the game assembles a combined spritesheet with translated sprites
replacing English ones as needed, and this sheet is used to visually
substitute the normal sprites at rendering time.
It's important to note that even if 32x32 enemies have pixel-perfect
hitboxes, this is only a visual change. This has been discussed several
times on Discord - basically we don't want to give people unfair
advantages or disadvantages because of their language setting, or
change existing gameplay and speedruns tactics, which may depend on the
exact pixel arrangements of the enemies. Therefore, the hitboxes are
still based on the English sprites. This should be basically
unnoticeable for casual players, especially with some thought from
translators and artists, but there will be an option in the speedrunner
menu to display the original sprites all the time.
I removed the `VVV_freefunc(SDL_FreeSurface, *tilesheet)` in
make_array() in Graphics.cpp, which frees grphx.im_sprites_surf and
grphx.im_flipsprites_surf. Since GraphicsResources::destroy() already
frees these, it looks like the only purpose the one in make_array()
serves is to do it earlier. But now we need them again later (when
switching languages) so let's just not free them early.
It'll start working in the next commit... See the description there.
(This commit does not add the new strings to the language files, since
Terry now added them separately in his own branch)
I recently changed my GitHub username from Dav999-v to Daaaav, and
today I moved the Ved repo from GitGud.io to GitHub. This commit
updates the references to both my username and the Ved repository.
The intention of the recent refactor was to make it so that the
temporary surfaces would be allocated only once, when the mode is
enabled, and be freed upon exit.
To do this, Graphics.cpp owns the pointers, and passes them to
ApplyFilter to modify. Except ApplyFilter doesn't actually modify the
pointers, because it's only a single pointer, not a pointer-to-pointer.
So every frame of rendering it would actually be creating a new surface
and leaking memory.
To fix this, they need to be pointer-to-pointer variables that get
modified.
I also added error logs in case the surface creation failed.
Right now, Windows assumes all our console output is code page ????.
That means our UTF-8 output appears mangled. (I ran into this while
testing IME text input by outputting strings to the console)
For a moment I was scared we'd need to do UTF-16 conversions and call
Windows-specific print functions like WriteConsoleW() in our vlog
functions, but fortunately SetConsoleOutputCP(CP_UTF8) works just fine.
Since VVVVVV 2.3, time trial best times are stored not just with the
number of seconds, but also the number of frames. However, there was
no room to display it with the old design of the time trials screen.
Now there is, so it can easily be displayed now with a small change!
Unset frames are stored as -1, which fits perfectly into the frames
argument of help.format_time(), because in that case the amount of
centiseconds is not shown.
It should be noted that opening VVVVVV 2.2 will instantly wipe your
frames records, as described by #1030. But many people will likely
never open 2.2 anymore.
Some people might be confused by the reference to M&P in the
instructions referring to downloading data.zip (#1026).
data.zip is the same between M&P and full versions of the game and is
orthogonal to which version of the game is built. Building M&P just
requires uncommenting `#define MAKEANDPLAY` in `MakeAndPlay.h`.
So clarify that you can grab data.zip from your existing copy of the
game, or from the Make and Play _page_ (not necessarily the Make and
Play edition of the game), and add instructions for building the M&P
version.
Closes#1027.
This serves as a file to help others in building the C++ Android version
for themselves.
These instructions are what I figured out to get it to work for me, and
should be kept up to date.
This is somewhat convenient for development, as it means that Android
Studio will only do a native build for the architecture of the device
being used for testing.
This is ignored for AABs, so it won't affect release builds (at least
for Google Play).
I took a very critical look at all the menus, to make sure they're all
clear and easy to read. I mainly simplified some explanations and
solved some small issues.
Well, 100% up-to-date with current upstream at least, there's some more
strings to be added soon, including "{area}, {time}" from #1018, a new
menu option related to translatable graphics files, and definitely some
credits stuff.
These were causing false alarms in translations for one reason or
another (either to force translations to not wordwrap for style
reasons, or to stay on the safe side if an adjacent string was also
long), so they can be raised now.
In 8484b36198, I fixed the title screen
showing up if you go to the language screen from in-game, while not
having any language files. There was also one other possible way to get
this to happen that I missed though: if you do have language files, and
you have not set your language yet, and you start a playtest via the
command line (e.g. by using Ved), and you then change the language
from the in-game options. That is now fixed.
I really thought I was going to need to block changing the language
in-game altogether, but activity zone prompts are now fixed and the
only obvious problem I can think of right now is having a dialogue
open, so I just disable the language option if a textbox is displayed.
(like how the map menu only has the save option if a script is running)
The translations for the prompts used to be looked up at creation time
(when the room is loaded or the activity zone is otherwise spawned),
which meant they would persist through changing the language while
in-game. This would look especially weird when the languages you switch
between use different fonts, as the prompt would show up in the old
language in the new language's font.
This problem is now fixed by letting the activity zone block keep
around the English prompt instead of the translated prompt, and letting
the prompt be translated at display time. This fixes a big part of the
reason I was going to disable changing the language while in-game; I
might only need to do it while textboxes are active now! :)
If someone makes a build of the game without copying the correct
folders, their version will have no translations, and display some text
wrong (like credits or button glyphs, or any custom levels that rely on
characters in the fonts being there). So I added a message in the
bottom left corner of the title screen to warn for that.
If you've never set a language before (<lang_set> is not 1), then the
language screen will show up before the title screen. Selecting the
language will then make the title screen show up.
If no language files are present, the old logic for handling this was
to simply show the language screen at startup anyway, and let it
display the error message that language files are missing, as a warning
that the game is not packaged correctly. However, this logic has two
flaws:
- If the user has ever had language files and set a language before
(in a VVVVVV on that computer), the warning element is gone because
the language screen is not shown in that case - the game is simply in
English
- If the user has never set a language before, and then goes to the
language screen later via the menu, they will be sent to the title
screen, even if they were in-game. The main menu will also be broken.
The new way is to not show the language screen at startup if language
files are missing, and to change the logic so that you will only be
sent to the title screen if you actually haven't seen the title screen
yet.
I will also add a proper warning that fonts or language files are
missing by adding a message in the bottom left corner (in place of the
MMMMMM installed message).
strings.xml in Polish didn't yet contain the Steam Deck strings, and a
few limits changes.
Also, "Font: " was translated as "Czcionka:", missing the space. This
is fixed now.
We had two separate cases for translators for this string (a
"TO UNLOCK:" one and a secret lab trophy one) but I forgot to use
the latter in the code, so both places in the game were using the
former. This is now fixed.
And another new language!
This uses the same font as Simplified Chinese. As such, I changed the
displayed name of the font (in the level editor) from 简体中文 to 中文.
Another new language! And this is a very interesting one, since it's
based on Nicalis' translation for 3DS and Switch (with their go-ahead).
Which means I had to convert between two completely different
language file formats, which was some work, but it's totally worth it!
Naturally, there are a lot of missing strings, so a translator will
still need to fill in all the blanks (and maintain the translation for
new strings of course)
The next commit will be the initial commit adding the Japanese
translation, so here's the font!
Specifically, it's k8x12L (2021-05-05), and it can be downloaded (in
non-VVVVVV format) from https://littlelimit.net/k8x12.htm
New language! This uses the 10x10 font added in the previous commit.
This has some minor changes from the delivered version:
- Synced language files, and thus added max_local attributes
- Removed leftover Catalan strings in strings_plural (and reduced to
just one form)
- Aligned terminal_finallevel
The Korean localization has been delivered (next commit), so this
commit adds the font for it! The chosen font is Galmuri 9 (specifically
GalmuriMono9, version v2.38.7). It's licensed OFL, and since I had to
convert it to VVVVVV's bespoke font format and shift characters around,
I think we are now bundling a Modified Version of the font, and it has
to use the same license. Including it as font_ko_license.txt and
clearly indicating that the copyright came from the Original Version
should be more than enough.
This version is a bit more polished than the placeholder one posted on
Discord, namely (non-CJK) characters were shifted to fit into their
10x10 bounds as much as possible, and notably the , and . characters
were shifted 2 pixels to the right.
This adds the following new strings from #993:
- The level editor is not currently supported on Steam Deck, as it
requires a keyboard and mouse to use.
- The level editor is not currently supported on this device, as it
requires a keyboard and mouse to use.
Unfortunately this means most languages won't be quite 100% anymore
for a bit, and updates come in which don't have this string yet.
But at least we can track it really well. In the next couple of
commits, when a language is updated with all new strings except for
these, I'll call them 99.9% instead of 100% (I did not get an actual
percentage).
Originally, we were using just the hint, but this didn't work well for
toggling VSync (see #831). Then I added SDL_RenderSetVSync to SDL, and
used that instead for toggling, but we were still setting the hint on
game startup.
Now, to keep things consistent, and just to make sure we don't get any
surprising behavior should things change in the future, this makes it so
game startup uses SDL_RenderSetVSync too.
This fixes#1013 by axing the use of SDL_HINT_RENDER_SCALE_QUALITY and
instead using SDL_SetTextureScaleMode.
The hint is unwieldy to use, and since #923, has resulted in a
regression where starting the game in filtered mode then switching to
nearest results in scaled textures still being filtered.
The proper solution is to use SDL_SetTextureScaleMode on the two
textures that are drawn to the final screen: gameTexture and
tempShakeTexture.
This commit fixes an obscure bug with `destroy(moving)` and
`destroy(disappear)` where, when looping through entities, the code
doesn't actually check what the entity is before trying to destroy the
block underneath it.
To fix this, we just put the block-destroying code *inside* of the
check, instead of being outside of it.
I also fixed the code style because it was horrible.
Closes#925.
My fix here is to delay the font change until all fading-out textboxes
have disappeared. See it as adding a sort of `untilbars` or `untilfade`
for text box fadeout, into setfont.
This doesn't prevent every possible way to change the font of an
existing textbox, but you would need to use internal scripting to still
do it (and basically be doing it on purpose) - the problem in
simplified scripting when you simply do textbox-setfont-textbox is
gone.
Showing the option on the "play a level" option feels to me as though
inexperienced players would think they're not supposed to open the
player levels, because the message says editor levels are unsupported,
right? But the message is only referring to the level editor, so in my
opinion, it's clearer to only show it there.
This commit removes the `NO_EDITOR` and `NO_CUSTOM_LEVELS` defines,
which cleans up the code a lot, and they weren't really needed anyways.
This commit also disables the editor on the Steam Deck, and adds a
program argument to re-enable the editor, `-enable-editor`.
For some reason, the credits button was always specifically removed from
M&P builds. After some discussion with Terry Cavanagh on the VVVVVV
Discord server, we agreed that there was no reason this should be
removed. So, it's getting put back in.
This puts the code to fix mouse coordinates in stretch mode directly
inside KeyPoll::Poll, preventing the need for any other instances of
mouse coordinate usage to copy-paste code.
If this text on the time trial results screen would overlap with the
time value, all rank labels will be displayed on the header line
instead ("TIME TAKEN:" etc). This works because the overlap with the
time most likely only happens with CJK fonts (where the time will be
very wide because of the font size) while strings like "TIME TAKEN"
take up very little space due to only needing 4 characters or so for
the same information.
- ERROR/WARNING screen title was overlapping with message
- Crewmate screen names and rescued statuses were overlapping with each
other
- Textboxes on Level Complete screen were overlapping with each other
and the crewmate was not vertically centered in the box
- Some strings were running into each other in flip mode, instead of
being moved out of each other (PR_CJK_HIGH and PR_CJK_LOW worked the
wrong way around because of FLIP macros being applied to Y coords)
- In-game esc menu was "bouncy" with selected menu options because of a
hardcoded 16 pixel offset
- Bindings in the gamepad menu were overlapping with each other
- Some Super Gravitron "Best Time" labels and values were a little too
close
This will be needed for the Simplified Chinese translation, of which
the first version has just been delivered!
This is the first language with a font bigger than 8x8 (this is 12x12),
so it might be a little rough in some places. Most of the game is
already prepared for it, though!
The only changes I made from the previous version (which was uploaded
on Discord a few times and also sent to the translator) was the …
character - it's often used twice in a row, and it was a little uneven
(looking like - - - - - - instead of - - - - - -); and the
semicolon, which was missing some pixels.
Two changes:
- The labels on the Game Complete! screen for number of trinkets/deaths
/time etc have been moved two pixels to the right, and had their
limits increased by 1 character
- The inaccuate limit for "quit to main menu" has been increased
These are errors and issues that have been reported, but are fixable by
us without needing to involve the translator in the fix, without too
much risk of accidentally breaking grammar rules.
The full list of fixes:
- Fixed some menus going offscreen by removing unneeded words (I'm
fairly confident after some cross-referencing and research) in
Portuguese BR, Portuguese PT, Spanish, and French
- Set 0 to singular in numbers.xml in Portuguese BR and French
- Removed the ** from dimensional stability generator to make it not go
offscreen, and aligned the text better, in Portuguese BR and Spanish
- Fixed some small casing and spacing errors in Portuguese BR, French
and Welsh. Think of misplaced spaces or sentences starting with a
lowercase letter
- Fixed a limit break in Spanish (the menu button already drops the
word "de" in "retraso de la entrada", so the title can too, right?)
- Corrected some typos in French and German (je continuer->je continue,
9: Finde->9: Feinde and NENÜ->MENU)
- Fixed spacing and alignment in teletype terminals in Welsh (like the
dimensional stability generator)
923efe54d6 added a note to the PR
template, but the best place is probably in the translators readme.
Hopefully this will make sure we no longer get people eager to start
a translation because nobody else had started one yet, and then have to
be disappointed with a reply from us saying we can't accept voluntary
translation contributions.
Another new language! It's based off of the old translator pack so
it'll need to be updated later. This commit also includes some fixes
to the originally submitted version, mainly:
- "2.0" and ".99" were broken by excel(?), now fixed
- Removed traces from extraneous rows and columns
- Small human errors
This includes the update received 2023-06-09.
For now, Terry has decided that translations can't be done by just
anyone. This note here should discourage at least some people from
making pull requests to add their own translations.
If you provided any one of -playx, -playy, -playrx, -playry, -playgc, or
-playmusic in command-line arguments for command-line playtesting, then
the game would always try to play music, even if you passed a negative
-playmusic. This wouldn't do anything in that case, unless you had
MMMMMM installed, in which case it would play MMMMMM track 15
(Predestined Fate Final Level) due to the legacy wraparound bug.
To fix this, only play music if the track provided is greater than -1.
Additionally, to prevent it from playing Path Complete by default if you
specify any of the other save position arguments but n ot -playmusic,
it's now initialized to -1 instead of 0.
Turns out `graphics.drawrect` exists. Well, not anymore!
This was another function from before the renderer rewrite which tried
to draw a rectangle by using four filled rectangles. We can draw
outline rectangles properly now, so let's make sure everywhere does it!
As #974 states, the lab background only ever uses the first generated
value from `backboxint`, so let's change it to a normal variable
instead of an array. Also, stars don't need their width/height set to
2 constantly... they never change in the first place, Terry!
Some code still used rectangles to draw things like lines and pixels,
so this commit adds more draw functions to support drawing lines and
pixels without directly using the renderer.
Aside from making generated minimaps draw points instead of 1x1
rectangles, this commit also batches the point drawing for an
additional speed increase.
This fixes a bug where if a track was resumed, pausing it by unfocusing
the window (if enabled, of course) would not resume it after refocusing
the window.
This happens because resuming the music doesn't change currentsong back
from -1, and the window refocusing code checks that currentsong isn't -1
before resuming music.
haltedsong is only used when resuming music. It is set back to -1 when
resuming music or when playing a new track.
Autotiling was a mess of functions and if chains and switch statements.
This commit makes autotiling better by assigning each direction to one
bit in a byte, giving each different combination its own value. This
value is then fed into a lookup table to give fine control on which
tiles get placed where.
The lab tileset can now use the single tiles which were before unused
in the autotiler, and the warp zone's background tool now places the
fill used in the main game.
- Some levels used chars < 0x20 as non-collapsible spaces, those would
now show up as [?]
- I recently found out how making characters < 0x20 6 pixels wide
doesn't work if they're missing from the font altogether
Therefore, the font now starts at 0x00 again instead of 0x20, like it
used to.
Arguably it was an advantage that the game would look extremely messed
up if you made a mistake with the fonts. In particular, a common
mistake could be to copy the new Unicode font.png, but forget to copy
the corresponding font.txt. However, 2.3 didn't come with the unicode
font, 2.4 will, so it'll be a lot less common for people to need to
manually copy the font. And if they do, it's probably for their own
level, and they have something in mind for the font, and if it doesn't
work they'll know fast enough when whatever they're planning doesn't
work (and it would only affect their own level's text, not any menus).
New language! It's still based off the old translator pack so the
newest strings haven't been translated yet, but the language files are
synced and we still need to get these updated in most other languages
either way.
This does include the update delivered on 2023-05-24.
A little while ago the term "AGOKLAVO" (action-key) was chosen to
replace the older "AGBUTONO" (action-button). However, in a recent
language update, I mistakenly used the older term in a new string.
This has been fixed.
Also note that it's written in the accusative case for this string
(with an "N" suffixed) since it is always used as the object of the
sentence where it appears ("Premu AGOKLAVON..." = "Press ACTION...").
This fixes a regression where main game teleporter icons (which would be
target icons if flag 12 was on) would be rendered on the minimap after
loading from a custom quicksave.
This is because this was always enabled when loading from a custom
quicksave, but the game didn't start rendering them until PR #898, which
de-duplicated the minimap rendering code.
The best fix here is to just not enable the teleporters when loading
from custom quicksaves.
This adds an anonymous enum for time trial indexes (e.g. the bestrank,
bestlives, etc. arrays and timetriallevel), and replaces all integer
literals with them.
Just like the unlock arrays, these are indexes to an array in XML save
files, so the numbers matter, and therefore should not use strict
typechecking.
This adds an anonymous enum for the unlock and unlocknotify arrays and
unlocknum function, and replaces all integer literals with them.
This is not named and thus cannot be used for strict typechecking
because these are actually indexes into an array in XML save files, so
the numbers themselves matter a lot.
This replaces the swngame int variable with a named enum and enforces
strict typechecking on it.
Strict typechecking is okay here as the swngame variable is not part of
the API surface of the game in any way and is completely internal.
And just to make things clear, I've added a SWN_NONE enum to use for
initialization, because previously it was being initialized to 0, even
though 0 was the Gravitron.
The clock on the Game Saved quicksave screen has always been upside-down
in Flip Mode. And technically, the trinket was too, but this was
unnoticeable because the default trinket sprite is symmetrical.
To fix this, draw flipsprites.png if these sprites are being drawn in
Flip Mode instead of sprites.png.
There's not any ill effects of it being initialized to 0 that I am aware
of (because in most cases, it either gets overwritten anyways or there
isn't a track playing in the first place), but it shouldn't be 0,
because that's Path Complete, so fixing this just in case.
This adds an anonymous enum for sound effects and replaces all calls to
music.playef that use integer literals.
This is not a named enum (that can be used for strict typechecking)
because sound effect IDs are essentially part of the API of the game -
many custom levels use these numbers. This is just to make the source
code more readable without needing a comment to denote what number is
what sound.
This adds an anonymous enum for music tracks and replaces all calls to
music.play and music.niceplay that use integer literals. Additionally,
this is also done for integer literals for cl.levmusic (except 0) and
music.currentsong where appropriate, but _not_ the music areamap because
that would not make it look very aesthetically pleasing in the code.
This is not a named enum (that can be used for strict typechecking)
because music track IDs are essentially part of the API of the game -
almost every custom level uses these numbers. This is just to make the
source code more readable without needing a comment to denote what
number is what track.
This adds a "- Press {button} to skip -" prompt to both the credits and
ending picture sequences.
It was always possible to skip them by pressing Enter, but not many
people knew this. In fact, even I didn't know this until I saw Elomavi
do it a year or so ago. So it's not really intuitive that this is
possible.
The prompt only shows up if you've completed the game before, and
disappears after two seconds similar to the "[Press {button} to return
to editor]" text.
Unfortunately, given how the game works, game completion is detected
based on if you have unlocked Flip Mode or not. At this point, the
unlock for the game being completed (unlock 5) will already be set to
true no matter what during the Plenary fanfare, but the Flip Mode unlock
(unlock 18) won't be until the player hits "play" on the main menu. As a
special case, the prompt will always show up in M&P (because Flip Mode
is always unlocked in M&P).
This prevents deleting telesaves and quicksaves in special modes and
custom levels.
Otherwise, rolling credits in a custom level would still delete the main
game quicksave.
If you had the map button held before the game transitioned to the
credits and ending picture sequences, then you wouldn't be able to skip
them, because those gamemodes don't have logic to detect when you've
released the map button.
To fix this, just implement the map button release logic.
We do need a better input system soon...
This command was changed from setactivityposition(x,y) to
setactivityposition(y), but there's a small problem here:
```diff
else if (words[0] == "setactivityposition")
{
- obj.customactivitypositionx = ss_toi(words[1]);
obj.customactivitypositiony = ss_toi(words[2]);
}
```
This meant that the function still took two arguments, the first of
which was unused and the second of which was the Y position of the
activity zone. This is now fixed.
This fixes a long-standing bug where it's possible to play music during
the Game Over screen in No Death Mode. All you have to do is die while
music is fading out from one area to the next.
The easiest way to do this is in the entrance to Space Station 2, since
there's a music change to Passion for Exploring in Outer Hull (you will
need to go into the zone far enough to activate Pushing Onwards first),
which also contains spikes to die on.
Basically, it's a simple oversight because the nicefade system relies on
music fading out to start playing the next track, but in this case, No
Death Mode fades the music out without accounting for that. It's best to
just disable nicefade entirely when dying in No Death Mode.
Thanks to KSS for reporting this bug.
This calls Game::savestatsandsettings() after unlocking Master of the
Universe (the achievement for completing No Death Mode), instead of
before.
This is not a big deal since Game::savestatsandsettings() is called
anyway whenever the game is gracefully closed since 2.3, but it helps to
make sure people won't lose their achievement data.
2.3 already made it so that if you ran the `rollcredits` command during
in-editor playtesting, you wouldn't be returned to the title screen
while losing unsaved level changes. But there are plenty of other ways
to go back to the title screen from in-editor playtesting too. Namely,
gamestate 1015 (the gamestate after completing a level) and 82 (time
trial complete).
So just add the appropriate checks to those gamestates, and add a
catch-all check in Game::quittomenu(). Additionally,
Game::updatecustomlevelstats() should not update custom level stats
during in-editor playtesting (otherwise it would still happen even if
the game didn't bring you back to the title screen).
Editor notes will also be shown if the game prevents you from going to
the title screen.
Also, just to make things clear, I also added a level note for when the
level is completed during in-editor playtesting. This is just to make it
clear in cases where it might not be obvious that the game returned you
to the editor for this reason. E.g. you have a terminal that calls
gamestate(1013) in a level with 0 custom crewmates, but when you
activate it, it looks like the terminal didn't work for some reason and
just brought you back to the editor. But that's just only because you
literally just completed the level.
This fixes a bug where the wrong music can play on the title screen, as
reported by AllyTally on Discord.
The bug can be triggered by triggering a room transition right as
game.quittomenu() is called (which is easiest to achieve by placing the
player on an oscillating/"out of bounds" room border in a custom level
so they go back and forth between two rooms every frame, and triggering
gamestate 1013, which starts a fadeout to menu if all custom crewmates
are rescued).
When this happens, game.quittomenu() calls script.hardreset(), but the
rest of the frame still executes, even though we set game.gamestate to
TITLEMODE too (because game.quittomenu() was called by
game.updatestate() which was called by gamelogic(), and game.gamestate
is only checked at the start of the frame). This ends up triggering a
room transition, and since map.custommode is guaranteed to now be off
(because of script.hardreset()), the main game music area code kicks in,
and plays something that isn't Presenting VVVVVV.
The bug here is that we're resetting too early when we still have the
rest of an in-game frame to execute. So, instead, we should only reset
at the end of the frame, and this can be achieved with a defer callback.
This will actually do several things:
(1) Make the tile size checks apply to the appropriate graphics files
once again.
(2) Make the game print a fallback error message if the error message
hasn't been set on the levelDirError error screen.
(3) Use levelDirError for graphics errors too.
(4) Make the error message for tile size checks failing specify both
width and height, not just a square dimension.
(5) Make the error messages mentioned above translatable.
It turns out that (1) didn't happen after #923 was merged, since #923
removed needing to process a tilesheet into a vector of surfaces for all
graphics files except sprites.png and flipsprites.png. Thus, the game
ended up only checking the correct tile sizes for those files only.
In the process of fixing this, I also got rid of the PROCESS_TILESHEET
macros and turned them into two different functions: One to make the
array, and one to check the tile size of the tilesheet.
I also did (2) just in case FILESYSTEM_levelDirHasError() returns false
even though we know we have an error.
And (3) is needed so things are unified and we have one user-facing
error message system when users load levels. To facilitate this, I
removed the title string, since it's really not needed.
Unfortunately, (1) doesn't apply to font.png again, but that's because
of the new font stuff and I'm not sure what Dav999 has in store for
error checking. But that's also why I did (4), because it looks like
tile sizes in font.png files can be different (i.e. non-square).
game.quittomenu() correctly resets state, as it's the function that's
always used when quitting to menu. This fixes a bug where if a level
with assets failed to load, it wouldn't unload the assets.
This exports the previously-internal setLevelDirError function in
FileSystemUtils and uses it for if a level is not found or there was a
parsing error. Previously, if a level failed to load in these ways, it
would take you to the error screen with no error, while printing it to
the console. But this makes it more user-friendly.
As a bonus, the text is localizable, just like the existing usage of
FILESYSTEM_setLevelDirError for if a path couldn't be mounted.
After the scriptclass::startgamemode refactor, a lot of common code is
still being executed even if the level loading failed. This sets the
game-gamestate to TITLEMODE in gotoerrorloadinglevel(), and also returns
early just in case.
Fixes#975.
This is quite a simple bug: If you edit a script, then close it, you
will no longer be able to use the mute buttons (N and M).
The problem here is that in 2.3, key.disabletextentry() was called when
closing a script. However, #944 removed the call. Therefore, a
regression.
If you used command-line playtesting to load a level in a zip, the game
would print a warning saying the level wasn't found. This is because the
warning is printed when it tries to load a level before it loads zips,
inside the metadata load function itself.
To fix this, just move the responsibility for printing the error outside
the function, and put it on the caller.
This prints all binary blob check fails. It's an error if the game
rejects the header and will refuse to load it at all, and a warning if
the game continues on.
This also removes the EOF check (`offset + header->size > size`) as a
fatal error. It will only print a warning now. If the last header goes
past the end of file, it will be handled gracefully by PhysFS, which is
the same case in VVVVVV 2.2. This actually fixes a regression from 2.3
where certain custom level tracks that were working perfectly fine in
2.2 (e.g. Summer Spooktacular's track 15) refused to play since 2.3.
This checks the return value of PHYSFS_readBytes() in all cases, and
uses a wrapper to not duplicate common logic. If the read fails, then it
will vlog an error, else if the amount of bytes read was less than
expected, it will vlog a warning.
Additionally, the space allocated for a file in
FILESYSTEM_loadFileToMemory is SDL_calloc()ed instead of SDL_malloc()ed
so if there are less bytes than expected, the memory will at least be
zeroed. This also means we don't have to do the null termination,
because the last byte will already be zeroed.
The return value of PHYSFS_readBytes() when reading the headers of
binary blobs now assigns to `header->size`, so the call has been placed
after the increment to `offset` because `offset` needs the correct (i.e.
intended) size of the header.
In the past I was thinking we could use some kind of feature in VVVVVV
to track outdated strings across language files. But as it turns out, a
manual solution is actually *perfect* (in combination with automatic
syncing): duplicating strings and marking the old one as outdated. We
started doing it in recent PRs, so let's make it official by adding it
to the readme.
In Italian, "Credits" is "Riconoscimenti", which runs offscreen with
the 3x font size that this title uses in the rolling credits at the end
of the game. I'm not sure if the translators saw that specific
instance, or thought the limit complaint was about the main menu button
all along (which is more prominent and *does* stick out far enough that
the complaint could plausibly have been about that, from a translator's
perspective!)
Either way, it's solved now: this string's width is now checked, and if
it will run offscreen at 3x size, it will now be displayed at 2x size
instead. The limit has been increased from 13 to 20 in the language
files accordingly.
This already happened on 2023-03-16, but I held off on updating the
repo's version a bit long: I wanted to wait a little to batch it up
with the next update, but the next update only arrived today, so...
I noticed that in 2.3, the game would launch with default controller
binds upon first launch (e.g. no pre-existing unlock.vvv or
settings.vvv), but in 2.4, this wasn't the case, and the default binds
would only be set the next time the game was launched. This would result
in you being essentially unable to use the controller on first launch
save for the analogue stick and D-pad to move between menu selections
and move the player.
Bisecting pointed to commit 3ef5248db9 as
the cause of the regression. It turns out returning early upon error or
a file not existing didn't set the default controller binds, because
they were done at the end of Game::deserializesettings(). But the binds
would be set on the next launch because if the file didn't exist, a new
file would be written, not with the default binds, but then the next
launch would read the file, see there were no binds, and then set the
default binds accordingly.
To fix this, I made it so that the default controller binds are set when
Game is initialized. That way, it covers all cases where the game can't
read a settings file.
Ever since 2.3, if you bind a controller button to the "menu" action
(i.e. back/escape) you won't be able to change that button to any other
action, because pressing it anywhere in the binding menu will exit to
the previous menu, without applying the binding.
I know action sets will overhaul everything, but 2.4 may (probably
"should") come out before we have action sets. This part is very
broken, and the fix is very easy: just move the bind-assigning code to
above the menu-return-on-esc code, and add a return.
For some reason, the accessibility option that was meant to disable
flashes doesn't disable ALL flashes, only screen flashes and screen
shaking. This commit disables a lot more, most importantly randomness
in colors, the player flashing on death/respawn, and teleporters
flashing.
This updates the interpolation positions of the player when transforming
into and out of VVVVVV-Man.
Otherwise, it can be seen that the player "zips" quickly during these
transformations if the Secret Lab entrance cutscene is played with
screen effects off.
Unfortunately, 1de459d9b4 caused a
regression where musicclass::niceplay() didn't work, because fading out
the music would cause haltdasmusik() to be called, which would reset the
fade variables.
To fix this, haltdasmusik() will now only reset the fade variables if
it's not called from a fade, which is signaled with a function
parameter.
`platv` is a room property that controls platform speed, and it has
always worked (other than some weird storage issues due to a bug).
However, the editor has no way to edit it currently, so people had to
resort to editing the level file by hand, or with a third-party tool.
This commit simply adds an easy way to modify platform speed.
VED has a fill bucket subtool for tiles and backgrounds, which is
really useful when creating rooms. This commit adds a fill bucket as
well, with an adaptive tool highlight, unlike VED.
This fixes a regression from 2.3 where the very beginning of A New
Dimension isn't silent.
A New Dimension's level music is set to Predestined Fate, but there is a
script box the player touches right upon spawning that stops playing
music. Then after the player ascends upwards, they touch a script box
that plays Predestined Fate. But in 2.2 and before, the very beginning
is silent due to the script box that stops music.
However in 2.3, due to the changes made to playing music during a fade,
the initial level music trying to play Predestined Fate during a music
fadeout from the main menu resulted in Predestined Fate being stored in
the nice fade variables, which kicked in after halting the music since
halting didn't reset those variables.
This resets those variables whenever music is halted, and now the
beginning of A New Dimension is back to how it was in 2.2 and before.
This fixes a regression where the red channel 0 glitch didn't work,
because the surface was always whitened, because LoadSprites would
whiten the image before converting it to surface.
This regression happened because of #923.
Fixes#962.
Misa asked me if this should only work for non-transparent textboxes,
and it shouldn't - that was kind of an oversight.
To make it work for transparent textboxes as well, I made a little
restructuring to avoid duplicating the code - fill_buttons() is now
called textbox_line(), and it replaces the direct accessing of the
textbox lines in the printing loops. The code that checks the width
of the textbox does not need to be copied, since the text box is
naturally not drawn for transparent text boxes.
This makes it so that `CustomEntity`s, at least internally, do not use
global tile position. Instead, they will use room-x and room-y
coordinates, which will be separate from their x- and y- positions.
This makes it much easier to deal with `CustomEntity`s, because you
don't have to divide and modulo everywhere to use them.
Since editorclass::add_entity and editorclass::get_entity_at expect
global tile position in their arguments, I've added room-x and room-y
arguments to these functions too.
Of course, due to compatibility reasons, the XML files will still have
to use global tile position. For the same reason, warp token
destinations are still using global tile position too.
As reported by Lilithtreasure on the VVVVVV Discord server, it is
possible to get gray moving platforms and enemies in the main game.
This happens if you play the main game after loading a custom level with
a room that is gray at the same coordinates. E.g. if you play a custom
level with a gray room at (12, 4), then exit and go to Gantry and Dolly
in the main game which is also at (12, 4), then the platforms there
would be gray too.
This is because there is a missing map.custommode check.
The missing piece from sound effects was handling what to do when the
buffer ran out. Which seems to happen often when decoding from OGG,
unlike WAV. This handling involves callbacks to functions named
refillReserve and swapBuffers.
Without this code,some sound effects would be cut off early, as
documented in #953. This might also explain the division by 20 - which
I've copied too, just in case.
Now OGG sound effect tracks should be identical to music tracks (except
I've stripped the looping code out).
Fixes#953.
We were using this function to check if the format of the existing voice
is different from the format of the voice we intended to play. However,
whereas the format we use is the FAudioWaveFormatEx struct, the only
details we get from FAudioVoice_GetVoiceDetails is the
FAudioVoiceDetails struct, which has much less details and is missing
important things like whether the format is 8-bit or 16-bit or something
else.
And of course, if we don't check that the number of bits matches, then
it still leads to playback issues as described in #953.
There are no other functions in FAudio that operate on
FAudioWaveFormatEx structs. So instead, we'll just have to do it
ourselves, and store the format of the existing voice alongside the
format of the intended voice. And then use SDL_memcmp to make sure the
formats are the same before playing, and if not, then destroy and
re-create the voice.
Fixes#953.
There's a few places where textboxes are constructed through code, but
they pass in the color's RGB values in manually. This commit
unhardcodes most of them them, replacing them with a color lookup.
The ones that weren't changed are special cases, like `175, 174, 174`.
I thought 2.2 already had separate map and interact gamepad bindings,
and they simply got neglected and broken with 2.3's split Enter/E key
option. But actually, the new split Enter/E option also applied to
gamepad buttons, and a separate interact binding was added, without
really indicating anything if Enter and E are not split. And I guess
using the same button for map and interact by default also makes sense
for simplicity...
This commit makes sure the button glyph displayed in-game is at least
the correct button. The gamepad bindings menu is also slightly modified
to darken the interact option - the button glyphs code now
automatically causes them to show equal buttons anyway, so it wasn't
too big of a change to also darken the line and disable the binding
option. To me this says: "the interact key is fixed to be the same as
enter right now, but there is a way to change it."
It's still not ideal of course, and I know a similar change to the
gamepad menu to hide the interact option was rejected a year ago
because action sets would already fix it, but it's a year later now,
and showing misleading button glyphs should be fixed in 2.4, whether it
will already have action sets or not (And at this point I think the
plan already is to keep the existing input system for 2.4)
And it's a 3 line diff to darken and disable the option, compared to
fully hiding the option.
The language screen has a "Press Space, Z, or V to select" hint, which
I forgot to update for supporting button glyphs in #943, so this commit
does.
<action_hint>Press Space, Z, or V to select</action_hint>
<gamepad_hint>Press {button} to select</gamepad_hint>
> CMake is re-running because D:/a/VVVVVV/VVVVVV/desktop_version
> /build_default/CMakeFiles/generate.stamp is out-of-date.
> the file 'D:/a/VVVVVV/VVVVVV/desktop_version/CMakeLists.txt'
> is newer than 'D:/a/VVVVVV/VVVVVV/desktop_version/build_default
> /CMakeFiles/generate.stamp.depend'
> result='-1'
"newer" probably means "the last edit date is newer because it was just
cloned", so let's see if including it in the cache means the last edit
date will be kept the same too... We can freely do this, as the cache
key includes the hash of the file itself.
Since the initial `cmake -G "Visual Studio 17 2022" ..` seems to be a
real bottleneck on the Windows CI (sometimes taking multiple minutes
for seemingly no reason), cache the build folder based on the hash
of CMakeLists.txt. It should be possible to reuse the build folder if
CMakeLists hasn't changed, and if it does change (because someone adds
a source file or library or something), we'll just do that initial
cmake with -G once, and cache that version as well.
https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
For some reason, this test takes a dozen seconds or more. Maybe because
it needs to boot up powershell or something like that? No idea, but
it's just sitting there. The cache action has a return value that
indicates whether there was a cache hit, so there's no need to use
Test-Path, saving CI time.
EDIT: Maybe the SDL cache hasn't ever been working. It seems to cache
30 bytes (huh?) and the folder it caches is C:\SDL, not C:\SDL2-2.24.0
as the folder would look like. So it was probably downloading SDL every
time anyway?
Also, I upgraded the cache@v2 action to cache@v3. I added a cache@v3
myself and noticed this was still using cache@v2, so I figured, why not
future-proof it? And that's when I notice this warning: "Node.js 12
actions are deprecated. Please update the following actions to use
Node.js 16: actions/cache@v2." So it's pretty good timing.
This was easier than I expected - just add an optional buttons="1"
attribute to cutscenes.xml. It's treated like the speaker attribute -
it's only there as context for the translator, and for the cutscene
test.
Due to rebasing messiness and diff noise, it's probably best if pull
requests either don't sync all the language files at all (and only
modify the English ones) OR only do it as a final commit. It's still
something we need to figure out, lol.
If a controller button is pressed, a controller is connected (even at
startup!) or an axis is moved, the game will switch to displaying
controller glyphs. If a keyboard key is pressed or the last controller
is removed, the game will switch to displaying keyboard keys.
This adds mappings from SDL's Xbox-based SDL_GameControllerButton
constants, to glyphs for the following layouts:
- LAYOUT_NINTENDO_SWITCH_PRO,
- LAYOUT_NINTENDO_SWITCH_JOYCON_L,
- LAYOUT_NINTENDO_SWITCH_JOYCON_R,
- LAYOUT_DECK,
- LAYOUT_PLAYSTATION,
- LAYOUT_XBOX,
- LAYOUT_GENERIC,
There may still be errors in these, but they should be mostly correct.
I'm leaving it up to Ethan to make it show the correct button glyphs
for the correct controllers being connected (and possibly to fix these
mappings where needed).
Violet's dialogue now looks like this:
squeak(purple)
text(purple,0,0,2)
Remember that you can press {b_map}
to check where you are on the map!
position(purple,above)
textbuttons()
speak_active
The new textbuttons() command sets the next textbox to replace {b_map}
with the map button, and {b_int} with the interact button. The
remaining keys would be added as soon as they need to be added to
ActionSets.h as well.
This adds a function that converts an action (such as interacting
in-game) to the corresponding button text ("ENTER", "E") or button
glyph (PlayStation triangle, Steam Deck Y, etc). This function
currently only gives the existing ENTERs or Es, because I don't know
how best to detect controller usage, or whether the game is running on
a Steam Deck, or what buttons need to be displayed there. Still, it
should now be really easy to adapt the rendering of keyboard keys to
consoles, controllers, or rebound keys.
To identify the actions that currently need to be displayed, this
commit also adds the initial enums for action sets as described by
Ethan in a comment in #834 (Jan 18, 2022).
These visualize the horizontal gravity line kludge for rooms beside
eachother. When you enter another room, gravity lines which look like
they're connected between the rooms try to have the same activated
state.
Basically, if you're in room (1,4) and you go into (2,4), if a
gravity line in (1,4) is activated (gray, on cooldown) and it's
touching the gravity line in (2,4), that gravity line will also be
activated.
This uses DDA (https://w.wiki/6RSQ) to draw a line between the previous
frame's mouse position, and the current frame's mouse position. This
means that there will no longer be gaps in lines of tiles if you move
your mouse fast enough (which is actually rather slow, so it gets
annoying quickly).
The editor's timestep is no longer hardcoded to 24, as I assume that
was only done so there would be less gaps in lines of tiles drawn.
With interpolation, that is no longer an issue, so I've removed the
editor's special case for the timestep.
Scripts used a weird "hook" system, where script names were extracted
into their own list. This was completely unneeded, so it has been
replaced with using the script.customscripts vector directly.
The script editor has been cleaned up, so the cursor's Y position is
relative to the entire script, rather than what's just displaying on
the screen currently. This simplifies a lot of code, and I don't know
why it was done the other way in the first place.
The script selector and script editor cursors have been sped up, since
both lists can be massive, and waiting 6 frames per line is extremely
slow and boring. This is still slow and boring, but we don't have
proper input repetition yet.
This commit moves everything left out of the previous commit to the
state system. This means a bunch of new functions were added as well,
to avoid the code in each function becoming too huge. A lot of cleanup
was done as well, simplifying logic, merging duplicated code, etc.
This commit does NOT touch "script hooks", script editor logic and
autotiling, as those seem to be their own separate beasts.
While warp lines were being drawn, they also got resized to
automatically fit between collision. In renderfixed, gravity lines are
resized the same way. Doing logic while drawing is very poor practice,
so resizing of these has been moved into logic, and merged together.
Aside from some more cleanup, this commit also removes the very poorly
done right click emulation, when you hold CTRL and click. It never
worked well in the past, and even requires a right click to use, so
there's not really any point to keeping it around.
The drawer could definitely be improved further, however I cleaned up a
little bit of the code duplication. I'll have to take a closer look
some other time, but I'm pretty sure that the duplicated code at the
bottom can be removed with a few tweaks, but I'll do that carefully
in a different commit.
Tools were a mess, spread all over the code with hundreds of `else-if`
statements. Instead of magic numbers denoting tools, an enum has been
created, and logic has been extracted into simple switch/cases, shared
logic being deduplicated.
The base of a state system for the editor has been created as well,
laying a good path for further organization improvements. Because of
this, the entire editor no longer gets drawn underneath the menus,
except for a few pieces which I haven't extracted yet. Either way,
this should be good for performance, if that was a concern.
I have this annoying issue where the game will open on the wrong monitor
in fullscreen mode, because that monitor is considered to be display 0,
whereas the primary monitor I want is display 1.
To mitigate this somewhat, the game now stores the display index that it
was closed on, and will save it to settings. Then the next time the game
opens, it will open on that display index. This should work pretty well
as long as you aren't changing your monitor setup constantly.
Of course, none of this applies if your window manager is busted. For
example, on GNOME Wayland, which is what I use, in windowed mode the
game will always open on the monitor the cursor is on, and it won't even
be centered in the monitor. But it works fine if I use XWayland via
SDL_VIDEODRIVER=x11.
Previously, the game would not store the size of the window itself, and
would always call SDL_GetRendererOutputSize() (via
Screen::GetWindowSize()) to figure out the size of the window. The only
problem is, this would return the size of the whole monitor if the game
was in fullscreen mode. And the only place where the original windowed
mode size was stored would be in SDL itself, but that wouldn't persist
after the game was closed.
So, if you exited the game while in fullscreen mode, then your window
size would get set to the size of your monitor (1920 by 1080 in my
case). Then when you opened the game and toggled fullscreen off, it
would go back to the default window size, which is 640 by 480.
This is made worse, however, if you were in forced fullscreen mode when
you previously exited the game in windowed mode. In that case, the game
saves the size of 1920 by 1080, but doesn't save that you were in
fullscreen mode, so opening the game not in forced fullscreen mode would
result in you having a 1920 by 1080 window, but in windowed mode.
Meaning that not even fullscreening and unfullscreening would put the
game window back to normal size.
The solution, of course, is to just store the window size ourselves,
like any other screen setting, and only use GetWindowSize() if needed.
And just to make things clear, I've also renamed the GetWindowSize()
function to GetScreenSize(), because if it was named "window" it could
lead one to think that it would always return the size of the screen in
windowed mode, when in fact it returns the size of the screen whatever
mode it is in - fullscreen size if in fullscreen mode and window size if
in windowed mode.
And doing this also fixes the FIXME above Screen::isForcedFullscreen().
This fixes the following bug that only occurs on Wayland: If the game is
configured to be fullscreened and in stretch mode, on startup, it won't
be in stretch mode. It will appear to be in letterbox mode, but the game
still thinks it's in stretch mode.
This is because during the ResizeScreen() call on startup, for whatever
reason, the window size will be reported to be the default size (640 by
480) instead of the screen resolution of the monitor, as one would
expect from being in fullscreen. It seems like when the game queries the
window size, the window isn't actually in fullscreen at that time, even
though this is after fullscreen has been set to true.
To fix this, I decided to always update the logical size before
SDL_RenderPresent() is called. To make this neater, I put the scaling
code in its own function named UpdateScaling().
This bug has existed since 2.3 and does not occur on X11. I tested this
on GNOME Wayland, and for testing it on X11, I used Openbox in a Xephyr
session while running VVVVVV with SDL_VIDEODRIVER=x11.
It turns out this texture is only used as a temporary texture to draw
the screen with an offset before rendering it to the output target.
I thought it was used for drawing the map menu animation, but that was
only true of `tempBuffer`, and is no longer true of the new render
system.
When `blackout` is active, the screen (to simplify) stops getting drawn
to. The excecption is textboxes, which draw anyway. But since the
screen isn't being cleared, removed textboxes stay on screen, since
that texture isn't being cleared. In the SDL_Renderer PR, I
accidentally broke this behavior, so this commit fixes it.
Fixes#951.
Dav999 pointed out this potential issue on Discord.
While basically all memcmp implementations will terminate early here if
there's a null byte (because it's mismatched), it doesn't hurt to add
the check here.
This fixes a bug where the trinket collection text boxes, along with the
cutscene bars, would stay on-screen if the player warped to the ship
while they were up.
This only happens during the gamestate 0 anti-softlock checks, and only
if completestop is active in the first place, so text boxes aren't
cleared if the player is doing something that wouldn't lead to a
softlock otherise.
Fixes#921.
This fixes a regression where the behavior of duplicate player entities
is different, causing a gameplay section in Vespera Scientifica to be
impossible, as described in #903.
In the level, you are allowed to flip in mid-air. Vespera accomplishes
this by having two duplicate player entities stuck in a platform. One of
them is responsible for letting the player flip in one direction, and
one of them is responsible for letting them flip in the other.
In 2.3, this works because in order for a player entity to flip,
`game.jumppressed` is checked, and the entity will flip if
`game.jumppressed` is greater than 0, then `game.jumppressed` will be
set to 0. In this way, whenever a player entity flips, it will set
`game.jumppressed` to 0, so whenever the next player entity is
evaluated, `game.jumppressed` is 0 and thus _that_ entity will not flip.
This is because the for-loop surrounds both the `game.jumppressed` check
and flipping the entity. In 2.4 after #609 and subsequent patches,
however, this is not the case. Here, the for-loop only surrounds
flipping the entity. Therefore, the `game.jumppressed` check is
evaluated only once. So, it will end up flipping every player entity if
the entities are eligible. In this case, one of them is eligible twice,
resulting in the game flipping gravitycontrol four times, which is
essentially the same as not flipping at all (except for all the sound
effects).
Hence, the fix here is to make it so the for-loop surrounds the
`game.jumppressed` check.
Now, this doesn't mean that the intent of #609 - that duplicate player
entities have the same initial velocity applied to them when flipping -
has to be removed. We can just put the for-loops back in. But I
carefully implemented them in such a way that the overall function is
not quadratic, i.e. O(n²). Instead, there's a pass done over the
`obj.entities` vector beforehand to store all indexes of a player entity
in a temporary vector, and then that vector is used to update all the
player entities. In this manner, the function is still linear - O(n) -
over the number of entities in the room.
I tested this to make sure that no previous regressions popped up again,
including #839, #855, and #887. And #484 is still resolved.
Fixes#903.
This adds support for OGG files as sound effects (via renaming them to
the wrong .wav file extension), because in previous versions of the
game, SDL_mixer didn't care what the file extension was, and so some
people relied on this, as described in #900.
This is accomplished by copy-pasting the OGG loading code for music
tracks. For a bit of cleanliness, I put the WAV and OGG loading code in
separate functions.
This is mostly the same code, except that because sound effects don't
loop and can't be paused or resumed, there's no reserve buffer, and
there's no data for loop points.
Also, for some reason, the music loading code divided by 20 in the
`size` calculation. I found that this prematurely cut off sound effects,
and that it made more sense to just not do the division. I don't know
why it was there, but removing it works.
Also also, some OGG files don't work with this. Namely, ones produced by
FFmpeg. To test this, I just extracted 0levelcomplete.ogg from
vvvvvvmusic.vvv and replaced terminal.wav with it. And it works, so
hopefully I won't have to touch audio code again, although I might if
someone complains about this. But either way, I'm committing this
because it's better than it was before.
Fixes#900.
I noticed that this call wasn't using VVV_freefunc. I missed it earlier
when going through Music.cpp and checking for instances when
VVV_freefunc should have been used
(a926ce9851).
Sound effects already get recreated if the number of channels
mismatches, but the same could be true if the sample rate mismatches
too, which was the case with music tracks as described in #886.
So, just to be sure - and to be consistent with music tracks - sound
effects now check that the sample rate matches, too, and if not, will be
recreated.
This fixes an issue where sound effects of bit depths that weren't 16,
such as 8, were being played incorrectly, as described in #886.
The problem is that when loading the sound effect, we would always set
the bit depth to 16 no matter what! Instead, we should set the bit depth
to the actual bit depth of the file.
Fixes#886.
As described in #886, if a track was played when an existing track was
already playing, and the sample rates of the two tracks differ, then the
second track would play wrong and distorted.
This is because the second track would play with the sample rate of the
first. To fix this, halt the track if the sample rate is mismatched,
which destroys the voice. This results in the voice being recreated
later in the Play() function. The track is also halted if the number of
channels or bit depth is mismatched.
Fixes#886.
The style we have here is that functions with no arguments are to have
explicit `void` arguments, even though this doesn't apply in C++,
because it does apply in C.
Unfortunately, some have slipped through the cracks. This commit fixes
them.
This removes the `addnull` argument from `FILESYSTEM_loadFileToMemory`
and `FILESYSTEM_loadAssetToMemory`, and makes it so a null terminator is
always appended no matter what.
This simplifies things and removes the need for callers to make the
decision about null termination and what its implications are. Then you
get cases where null termination might not happen when it should be,
such as the one df577c59ef (#947) fixed.
When FIQ added the `addnull` argument in
5862af4445 (#117), I'm guessing he did it
because he wanted to be cautious about adding the null terminator to
every file, so he only did it for XML files, which was the only case
needed at the time. But really, there's no downsides to always appending
a null terminator. In fact, it's already always done whenever the STDIN
buffer is loaded.
Because of how `blackout` works, screen shaking must clear the gameplay
buffer. `blackout` simply pauses rendering, so if the gameplay buffer
gets cleared, then the screen will just be black, otherwise it'll look
like the game is "frozen". VVVVVV only uses `blackout` during screen
shaking, so it works as intended. However, when reimplementing this
behavior in the move to using the SDL_Renderer system, I failed to
notice that since my implementation always clears the gameplay buffer
when shaking, if you open the menu during a shake, instead of seeing
gameplay during the transition animation, you only see black. This has
been fixed with a simple `game.blackout` check before clearing the
gameplay buffer.
For some reason, originally, this function mutated the std::string
passed into it by reference. So calling the function could potentially
mutate whatever got passed in, and callers potentially could have relied
on that behavior.
Now that the surrounding callsites have all been cleaned up, though
(especially scriptclass::startgamemode), it's clear that it's only used
in two places: Loading the level in the editor, and loading the level in
live gameplay. In both cases, the passed-in string isn't used ever again
afterwards.
So, it's safe to delete the mutable reference without any undesirable
effects, making the code cleaner and easier to understand. The function
now mutates a copy instead of mutating whatever the caller has.
An example is Maximally Misleading Miserable Misadventure, which has a
font.txt which includes all ASCII characters starting with a 0x00 byte.
This would accidentally null-terminate the string too early.
Instead, we now use the total length of the file again, and keep
getting the next UTF-8 codepoint until the file ends. We still need to
null-terminate it - it protects against incomplete sequences getting
the UTF-8 decoder to read out of bounds.
This was an oversight when we migrated to the new UTF-8 system - it
expects a null-terminated string, but the utfcpp implementation worked
with a pointer to the end of the file instead.
I also added an assert in FILESYSTEM_loadFileToMemory() so this is less
likely to happen again - because there should be no valid reason to
have a NULL pointer for the total file size, as well as not wanting a
null terminator to be added at the end of the file.
Courtesy of Reese. Mainly the accented characters are updated -
uppercase letters are now mostly a pixel higher to make them higher
than lowercase letters (and only a single pixel lower than uppercase
letters without accents, instead of two pixels lower). Accents for
lowercase letters have also been made thicker overall and changed in
appearance.
Also: this font image is converted to indexed grayscale instead of full
RGBA, which makes the file only 4.5 KB instead of about 10 KB.
This updates all language files to the latest version.
- Some minor errors are also fixed - for example, a small number of
changes were made to the English string instead of the translation.
Alignment of the dimensional stability generator terminal is also
improved in several languages.
- I also discovered that the string "Complete the game" appears twice -
and has, to be consistent with adjacent strings, two separate
translations in Portuguese (PT). So this string now properly has two
different cases so it can be translated separately.
- The limit for TIME/SHINY/LIVES has been bumped from 7 to 8
The following languages are new:
- French
- German
- Italian
- Portuguese (BR)
- Portuguese (PT)
- Russian
- Spanish
- Turkish
Esperanto has also received some updates.
`hardestroom` currently stores the current roomname, but it was missing
a call to get the translated string. This has been added, fixing the
hardest room appearing as untranslated.
See the previous two commits, a lot of the time we don't need
std::string objects to be passed to these functions because we already
have C strings.
Commit 1/3: font::print_wrap
Commit 2/3: font::print
-> Commit 3/3: font::len
Turns out I was overplaying my hand a little when changing font::print
from std::string to const char*, so instead, I'll overload the
function: it can take either a const char* (the main function) or a
std::string (a wrapper). This means any C string that's printed
everywhere else (which is common, especially because loc::gettext gives
them) no longer needs to be converted to a std::string object each call.
Commit 1/3: font::print_wrap
-> Commit 2/3: font::print
Commit 3/3: font::len
We no longer need to pass a std::string object to the print and len
functions - in fact, we often only have a C string that we want to
print or get the visual width of (that C string most often comes from
loc::gettext), and it's a bit wasteful to wrap it in a new std::string
object on every print/len call.
This does mean adding a few more .c_str()s, but there's not many places
where a std::string is being passed to these functions, and we already
use .c_str() sometimes.
-> Commit 1/3: font::print_wrap
Commit 2/3: font::print
Commit 3/3: font::len
This removes memory churn caused by using analogue mode.
The surfaces are only allocated if analogue mode is turned on, and kept
after they are initialized. Otherwise, if analogue mode is never turned
on (which will be the case for the vast majority of the time the game is
played), then no extra memory is used.
Drawing a texture onto itself seems to produce issues on Metal.
To fix this, use a temporary texture instead, that then gets drawn onto
the original texture.
Fixes#927.
These are from a fan translation that was originally made in 2020.
The files were kept around as a possible base for the future Spanish
translators, and now that a first version of the new Spanish
translation is being tested, it's time to remove this one. These files
are making it very annoying for me to test all the new translations and
then jump around between different commits (and they're already not in
the round 2 translator pack).
Actually, this won't really help with the jumping around different
commits part... But the sooner it's removed, the less confusing it will
be when different versions of the language pack are floating around and
the latest version needs to be added, and the less "Changes not staged
for commit" problems you'll get when testing the new language packs.
(Or two different españols being on the language screen)
This is because destroying the renderer causes use-after-frees since the
renderer destroys all textures when it gets destroyed.
This fixes a Valgrind error where an invalid read occurs because the
font textures get destroyed again after the renderer is destroyed.
The hashmap would get populated with the name of each font, as each
font was being added. Unfortunately, adding a font would also realloc
the storage for fonts, in which the names are also stored... Possibly
invalidating the pointers to the names. This is now fixed by populating
the hashmap after all the fonts are added.
For consistency, since they are created in create_buffers as well. I
checked with Valgrind (which is very noisy on Wayland, it turns out),
but I didn't see anything about them not being freed. It doesn't hurt to
use VVV_freefunc here anyway, though, since it does a NULL check and
nulls the pointer afterwards, which should prevent double-freeing and
use-after-frees.
I'm going to soon be creating an actually temporary texture, so having
two textures named "temp" would get confusing. This is also a good
chance to correct the name of this texture, because it's not really
temporary, but it's used for map menu animation rendering.
This commit replaces the old system with the new one, making it much
easier to edit the transforming and glitchy roomnames. Additionally,
this syncs flag 72 to finalstretch.
Co-authored-by: Misa Elizabeth Kai <infoteddy@infoteddy.info>
This commit adds a better system for animated roomnames.
The old system, like many other systems, were very hardcoded, and can be
described as mostly else-if chains, with some fun string comparisons.
The new system uses lists of text for transformations and glitchy names,
making it much easier to add new cases if needeed.
This commit implements the system but does not replace the old system,
where that is done in the next commit.
The settings for special roomnames can be read from level XML, and
`setroomname()` can be used from commands to set a new, static name.
I'm also planning to change the argument types of font::len,
font::print and font::print_wrap from const std::string&s to
const char*s, but I'll do that separately.
This is a small library I wrote to handle UTF-8.
Usage is meant to be as simple as possible - see for example decoding
a UTF-8 string:
const char* str = "asdf";
uint32_t codepoint;
while ((codepoint = UTF8_next(&str)))
{
// you have a codepoint congrats
}
Or encoding a single codepoint to add it to a string:
std::string result;
result.append(UTF8_encode(0x1234).bytes);
There are some other functions (UTF8_total_codepoints() to get the
total number of codepoints in a string, UTF8_backspace() to get the
length of a string after backspacing one character, and
UTF8_peek_next() as a slightly less fancy version of UTF8_next()), but
more functions could always be added if we need them.
This will allow us to replace utfcpp (utf8::unchecked) and also fix
some less-than-ideal code:
- Some places have to resort to ignoring UTF-8 (next_wrap) or using
UCS-4→UTF-8 functions (VFormat had to use PHYSFS ones, and one other
place has four lines of code including a std::back_inserter just for
one character)
- The iterator stuff is kinda confusing and verbose anyway
Originally the changedir command was used here, making
Vitellary look left and then immediately snap back to
looking right. Now the changeai command is used instead
to make him actually look left, and then look back to
the right on his last textbox.
The `-addresses` command-line option added in 64be99d4 helps
autosplitters on platforms where VVVVVV is not built as a
position-independent executable. macOS has made it increasingly
difficult, or impossible, to build binaries without PIE.
Adding an obvious string to search for will help tools that need to deal
with versions of VVVVVV built with PIE. The bytestring to search for is
`[vVvVvV]game`, followed by four null bytes (to avoid finding it in the
program code section). This identifies the beginning of the game object;
addresses to other objects can be figured out by relative offsets
printed by `-addresses`, since ASLR can only change where the globals
begin.
Partially fixes#928; it may still be advisable to figure out how to
explicitly disable PIE on Windows/Linux.
Activity zone prompts have always been limited to a single line,
because the text box had a hardcoded size. A translator requested for
the possibility to add a subtitle under music names for the jukebox,
and the easiest solution is to make it possible to translate a prompt
with multiple lines. This is possible now, and the textbox even
wordwraps automatically.
(I wouldn't really like to see translations using multiple lines for
stuff like "Press ENTER to talk to Vitellary", especially if it wraps
with one name and not with another, but if a string is too long,
wordwrapping will look better than text running out of the box.)
There were two print calls, one for the transparent case, and one for
a regular textbox. The print calls were nearly the same except for the
color, and for some reason the transparent case didn't have PR_CJK_LOW
(that one is on me).
There is no overlap in side effects between this line and the switch
statement after it, but it did result in adding the width of a final
null terminator or newline to the width of the current line, which is
a waste because those widths both 1) require trying to find
non-existent characters in the font and 2) will not be used.
I found this out because I added a debug print in find_glyphinfo(), and
something was requesting lots of codepoint 0 from the font.
In a button glyph font (like buttons_8x8.fontmeta) you can now specify
<type>buttons</type> to indicate that it's a button glyphs font. In a
normal font, you can specify <fallback>buttons_8x8</fallback>. This
will make it such that if a character is not found in the main font,
it will instead be looked for in buttons_8x8. If not found there
either, the main font's U+FFFD or '?' will be used as before.
This makes find_font_by_name() not O(n). It's not really a big deal,
because there won't be many fonts, but it'd make a function in the next
commit (finding the given fallback font for each font by name) O(n^2).
It's easy enough to add the hashmap.
They are not used yet in this commit - this just adds the graphics and
data for the glyphs. It also adds a <fallback> tag to font.fontmeta to
use buttons_8x8.
The icons themselves are made by Reese Rivers - see #859.
This makes them stand out more.
The border around the tool has also been moved to be drawn first.
Otherwise, it would be drawn on top of the outline of the text, which
would look bad.
This prints the address of every global class to the console, and then
exits.
This is useful for autosplitters, which read memory addresses directly.
Any time a new version of the game is shipped, this makes updating the
autosplitters much easier as people don't have to find the addresses of
the global classes themselves.
After discussing with Ally and Dav, we came to the agreement that this
is basically useless since the prompt will always be centered and take
up most of the horizontal space of the screen.
And the x-position was only added as an offset because at some point,
there was a missing space from the side of the "- Press ENTER to
Teleport -" prompt, and the offset was there so people could mimic the
prompt accordingly. But that was fixed at some point, so it's useless
now.
Even though it would be a bizarre combination, declaring no character
set (neither via <chars> nor via font.txt) meant that <special>
couldn't be used because the ASCII fallback charset would be loaded
after special ranges were processed. Now, all the methods of loading
the charset are attempted sequentially, and only afterwards, the
special ranges are loaded.
There used to be two ways of fading in/out text in VVVVVV:
- Local code that modifies the R, G and B values of the text
- Keeping the RGB values the same and using the alpha channel
The latter approach is only used once, for [Press ENTER to return to
editor]. The former approach causes problems with colored (button)
glyphs: there's no way for the print function to tell from the RGB
values whether a color is "full Viridian-cyan" or "Viridian-cyan faded
out 50%", so I added the flag PR_COLORGLYPH_BRI(value) to tell the
print function that the color brightness is reduced to match the
brightness of colored glyphs to the brightness of the rest of the text.
However, there were already plans to make the single use of alpha
consistent with the rest of the game and the style, so PR_ALPHA(value)
could be removed, as well as the bit signifying whether the brightness
or alpha value is used. For the editor text, I simply copied the "Press
{button} to teleport" behavior of hiding the text completely if it
becomes darker than 100/255.
Another simplification is to make the print function handle not just
the brightness of the color glyphs while local code handled the
brightness of the normal text color, but to make the print function
handle both. That way, the callsite can simply pass in the full colors
and the brightness flag, and the flag name can be made a lot simpler as
well: PR_BRIGHTNESS(value).
"by {author}" is a string that will cause a lot of localization-related
problems, which then become much worse when different languages and
levels can also need different fonts:
- If the author name is set to something in English instead of a name,
then it'll come out a bit weird if your VVVVVV is set to a different
language: "de various people", "por various people", etc. It's the
same problem with Discord bots completing "playing" or "watching" in
their statuses.
- Translators can't always fit "by" in two letters, and level creators
have understandably always assumed, and will continue to assume, that
"by" is two letters. So if you have your VVVVVV set to a language that
translates "by" as something long, then:
| by Various People and Others |
...may suddenly show up as something like:
|thorer Various People and Othe|
- "by" and author may need mutually incompatible fonts. For example, a
Japanese level in a Korean VVVVVV needs to be displayed with "by" in
Korean characters and the author name with Japanese characters, which
would need some very special code since languages may want to add
text both before and after the name.
- It's very possible that some languages can't translate "by" without
knowing the gender of the name, and I know some languages even
inflect names in really interesting ways (adding and even replacing
letters in first names, surnames, and anything in between, depending
on gender and what else is in the sentence).
So to solve all of this, the "by" is now replaced by a 10x10 face from
sprites.png, like a :viridian: emote. See it as a kind of avatar next
to a username, to clarify and assert that this line is for the author's
name. It should be a fairly obvious/recognizable icon, it fixes all the
above problems, and it's a bonus that we now have more happy faces in
VVVVVV.
If your language has a bigger font, the max attribute isn't really
helpful to you as a translator, so the sync feature adds a special
max_local attribute which is accurate to the font size. This was
already documented in advance.
If used, we now also write an attribute in the root tag of strings.xml
and strings_plural.xml, that looks like max_local_for="12x12". I
decided to add this attribute after finding out the Excel macros would
be really hard to change to only show a max_local column if it is ever
used (it'd need to look ahead through the strings until it finds a
string with a max, or remove the column if no string has used it), but
it's also a convenience for translators.
If, somehow, a single character is wider than the limit, next_wrap
would get you stuck in an infinite loop by refusing to update the start
index and giving a line length of 0. Now, it just gives you a line with
that single character.
I also made some small readability improvements: I renamed next_wrap_s
to next_wrap_buf, and added comments at the top of both functions
explaining what they do.
By default, when you open the level editor to start a new level, the
level font will now match your VVVVVV language; so if you're, say,
Japanese, then you can make Japanese levels from the get-go. If you
want to make levels for a different target audience, you can change the
font via a new menu (map settings > change description > change font).
The game will remember this choice and it will become the new initial
level font.
If a custom level doesn't specify a font, it should be the 8x8 font.
But the main game can't specify a font, it's just the interface font
because that's for the language that the game is in.
They need to know how wide the text is going to be in a particular
font, so font::string_wordwrap and font::string_wordwrap_balanced now
take a flags argument like all the printing and dimensions-getting
functions. next_wrap and next_wrap_s take a Font* now, they're internal
to Font.cpp so they can take a Font and avoid double flag-parsing. But
if any non-Font.cpp code needs next_wrap/next_wrap_s in the future, I'd
just make a public wrapper that takes a uint32_t flags and passes the
Font* to the internal functions.
This is one of the few places still using 2-space indentation instead
of 4 spaces, and it makes it very annoying to work with when your tab
key inserts four spaces - so I could either just mimic it for the time
being, or I could just fix it while I was at it.
- If the level font is higher than 10 pixels, the third description
line (Desc3) is disabled and unavailable. CJK languages require less
characters to convey the same message (140 characters caused people
to cram tweets in all languages except CJK) and this gives us enough
room in the levels list without having to cram the metadata even more
than it already was or showing less levels per page.
- The "Untitled Level" and "by Unknown" now selectively show up in the
interface font instead of the level font.
None of the structs in the new font system ended up being "publicly"
accessible, they were all treated as implementation details for
Font.cpp to use, so these structs are now fully defined in Font.cpp
only.
The last two deprecated functions are:
- Graphics::Print
- Graphics::PrintWrap
These are used a lot, but they're relatively easy to replace, since the
only flag I probably have to immediately worry about is PR_CEN. I do
often need to add PR_FONT_* flags but I don't need to add any
PR_2X/PR_3X/PR_4X anymore.
Only three deprecated functions remain:
- Graphics::Print
- Graphics::PrintWrap
- Graphics::bigprint
I also fixed multiline transparent textboxes having their outlines
overlap the text itself, and fixed textboxclass::padtowidth assuming
glyph widths of 8 (it made the hints at the start of intermission 1
run offscreen for example)
Only four deprecated functions remain:
- Graphics::Print
- Graphics::PrintWrap
- Graphics::bigprint
- Graphics::bprint
Graphics::bprint is the least-used one of them, and after that, the
other functions are used a LOT, but it'll be a lot faster to go through
them, since I have less and less flags to worry about. I'll probably
start using Vim macros again like I did for loc::gettext()ing strings,
or maybe I'll automate this completely.
I migrated all of them to font::print, so they can now be removed.
Six deprecated print functions left! (Of which some are used a whole
lot, it's simpler if the lesser-used ones are gone first.)
This used some constants counting numbers of characters
(LEN_INTERIM_COMMIT and LEN_BRANCH_NAME) and even an SDL_arraysize,
all multiplied by 8, to get the length of the displayed text.
Now it just uses the new PR_RIGHT flag of font::print.
I did also force the 8x8 font for all the interim information, since
the date kinda overlapped with the menu options... And all of these
lines only show up in interim versions anyway, except for the version
number - which is left in the interface font for consistency with the
rest of the menu in non-interim versions. The inconsistency in interim
versions doesn't really matter all that much I think, it's just some
technical/debug info.
I migrated all graphics.len calls over to font::len (and also migrated
prints mainly surrounding those graphics.len's) so the old len function
is now completely removed.
I especially focused on graphics.len and the print calls around them,
because graphics.len calls appear a bit less often, might be overlooked
when migrating print calls (thus possibly using different fonts by
accident) and are often used for some kind of right-alignment or
centering which can be changed into PR_RIGHT or PR_CEN with a different
X anyway.
Notably, I also added a new function to generate these kinds of
sliders: ....[]............
Different languages means that the slider for analogue stick
sensitivity needs to be longer to fit possibly long words for
Low/Medium/High, and then different font sizes means that the longer
slider won't fit onscreen in a language that needs a 12-wide font. So
slider_get() can take a "target width", which dynamically changes the
number of characters depending on the width of them in the interface
font.
I kinda forgot that I could force the 8x8 font instead of adapting the
characters in the slider to the font, and other ideas (like using
different characters or a more graphical progress bar) have been
brought up on Discord, so this might all change again sooner or later.
If you for example have your VVVVVV set to English, have the option for
Chinese selected, and then the window loses focus, the English pause
screen would show up in the Chinese font. This is now fixed.
meta.xml can now have a <font> tag, which gives the name of the font
that the language needs. This will directly control the interface font
when the language is active, and will soon also control the font used
for each option on the language screen.
Also added some borders to more of the text in room name translator
mode, fixed a positioning issue if the interface font is not 8x8, and
migrated the trophy texts to font::print_wrap (including
PR_COLORGLYPH_BRI that still needed to be done)
Activity zones need to be in the interface font if the message is from
the system (like Press ENTER to activate terminal, which may be in a
different language) and in the level font if it's a customized message
(setactivitytext).
Graphics::drawtextbox was counting the textbox width and height in
8x8 characters, even including the borders as characters, so it'd need
to be told what the font for the textbox is, and then probably only the
height needs to be adapted to the font and not the width because that's
adapted to the screen width... So just call Graphics::drawpixeltextbox
directly instead. It was already weird enough how actual cutscene
textboxes called Graphics::drawtextbox with x/8, y/8 before the
previous commit, (when you already have the pixel width and height!)
only to have that be a wrapper for drawpixeltextbox by doing x*8, y*8.
Some textboxes need to be in the level font (like room names, cutscene
dialogue, etc - even in the main game), and some need to be in the
interface font (like when you collect a shiny trinket or crewmate). So
most of these textboxes now have graphics.textboxprintflags(font_flag)
as appropriate.
RoomnameTranslator.cpp is now also migrated to the new print system -
in room name translator mode, the room name is now displayed in the 8x8
font if it's untranslated and the level font if it is.
Level text such as room names, text box content, and the contents of
the script editor need to be displayed in the level-specific font, and
tweaked to look right. This involves displaying less lines in the
script editor, making text boxes bigger, displaying some text higher
and some text lower. This is still unfinished, but it's the real start
of a migration to font::print functions!
find_font_by_name() just finds the index of a given font name. This
index is supposed to be stored and reused, because the font (for a
language/level) won't be changed very often. So this function would
only run when getting the language metadata, when loading a level, etc.
All global fonts and all custom fonts in a level are now loaded, and
added to their respective "vectors". The selected font is still always
as the global font.png, and the custom level font also isn't selected
yet, but it's now easier to implement that.
Also, I added FILESYSTEM_enumerateAssets, which #902 already has but I
needed it now. I also rewrote it to not use std::vector<std::string>.
That was my idea, it's also how FILESYSTEM_getLanguageCodes worked,
so for symmetry, that function is getting changed as well.
The font::len function now handles the printing scale, so it can
immediately return the scaled length instead of having the caller
calculate it. The print function now handles CJK low/high flags and
vertically centers CJK text by default (instead of letting it stick
out on the bottom).
The following functions were moved directly:
- next_wrap
- next_wrap_s
- string_wordwrap
- string_wordwrap_balanced
- string_unwordwrap
These ones will probably still need get a flags argument, except for
string_unwordwrap (since they need to know what font we're talking
about.
The implementation of graphics.len has also been moved to Font.cpp,
but graphics.len still exists for now and is deprecated.
graphics.PrintWrap is now also deprecated. An advantage of the new
version (with flags) is that it'll be possible to do things like put
a border around wrapped text, wrap text at larger scales, etc, but
these things don't work perfectly yet.
This commit also has some other fixes, like the default advance of
6 pixels for characters 0x00-0x1F in 8x8 fonts.
There has always been a mess of different print functions that all had
slightly different specifics and called each other:
Print(x, y, text, r, g, b, cen)
nothing special here, just does what the arguments say
PrintAlpha(x, y, text, r, g, b, a, cen)
just Print but with an alpha argument
PrintWrap(x, y, text, r, g, b, cen, linespacing, maxwidth)
added for wordwrapping, heavily used now
bprint(x, y, text, r, g, b, cen)
prints an outline, then just PrintAlpha
bprintalpha(x, y, text, r, g, b, a, cen)
just bprint but with an alpha argument
bigprint(x, y, text, r, g, b, cen, sc)
nothing special here, just does what the arguments say
bigbprint(x, y, text, r, g, b, cen, sc)
prints an outline, then just bigprint
bigrprint(x, y, text, r, g, b, cen, sc)
right-aligns text, unless cen is given in which case it just
centers text like other functions already do?
bigbrprint(x, y, text, r, g, b, cen, sc)
prints an outline, then just bigrprint
We need even more specifics with the new font system: we need to be
able to specify whether CJK characters should be vertically centered or
stick out on the top/bottom, and we sometimes need to pass in
brightness variables for colored glyphs. And text printing functions
now fit better in Font.cpp anyway. So there's now a big overhaul of
print functions: all these functions will be replaced by font::print
and font::print_wrap (the former of which now exists). These take flags
as their first argument, which can be 0 for a basic left-aligned print,
PR_CEN for centered text (set X to -1!!!) PR_BOR for a border (instead
of functions like bprint and bigbprint), PR_2X, PR_3X etc for scaling,
and these can be combined with |.
Some text, for example [Press ESC to return to editor], fades in/out
using the alpha value, which is passed to the print function. In some
other places (like Press ENTER to teleport, textboxes, trophy text...)
text can fade in or out by direct changes to the RGB values. This means
regular color-adjusted white text can change color, but colored button
glyphs can't, since there's no way to know in the print system what the
maximum RGB values of a specific textbox are supposed to be, so the
only thing it can do is draw the button glyphs at full brightness,
which looks bad. Therefore, you can now also pass in the brightness
value via the flags, with PR_COLORGLYPH_BRI(255).
Since there's now a new XML-based font metadata file format to obsolete
the .txt file with all the glyphs, this commit switches the built-in
font to that new format.
This is still a work in progress, but the existing font system has been
removed and replaced by a new one, in Font.cpp.
Design goals of the new font system include supporting colored button
glyphs, different fonts for different languages, and larger fonts than
8x8 for Chinese, Japanese and Korean, while being able to support their
30000+ characters without hiccups, slowdowns or high memory usage. And
to have more flexibility with fonts in general. Plus, Graphics.cpp was
long enough as-is, so it's good to have a dedicated file for font
storage.
The old font system worked with a std::vector<SDL_Surface*> to store
8x8 surfaces for each character, and a std::map<int,int> to store
mappings between codepoints and vector indexes.
The new system has a per-font collection of pages for every block of
0x1000 (4096) codepoints, that may be allocated as needed. A glyph on
a page contains the index of the glyph in the image (giving its
coordinates), the advance (how much the cursor should advance, so the
width of that glyph) and some flags which would be at least whether the
glyph exists and whether it is colored.
Most of the *new* features aren't implemented yet; it's currently
hardcoded to the regular 8x8 font.png, but it should be functionally
equivalent to the previous behavior. The only thing that doesn't really
work yet is level-specific font.png, but that'll be supported again
soon enough.
This commit also adds fontmeta (xml) support.
Since the fonts folder is mounted at graphics/, there are two main
options for recognizing non-font.png fonts: the font files have to be
prefixed with font (or font_) or some special file extension is
involved to signal what files are fonts. I always had a font.xml in
mind (so font_cn.xml, font_ja.xml, etc) but if there's ever gonna be
a need for further xml files inside the graphics folder, we have a
problem. So I named them .fontmeta instead.
A .fontmeta file looks somewhat like this:
<?xml version="1.0" encoding="UTF-8"?>
<font_metadata>
<width>12</width>
<height>12</height>
<white_teeth>1</white_teeth>
<chars>
<range start="0x20" end="0x7E"/>
<range start="0x80" end="0x80"/>
<range start="0xA0" end="0xDF"/>
<range start="0x250" end="0x2A8"/>
<range start="0x2AD" end="0x2AD"/>
<range start="0x2C7" end="0x2C7"/>
<range start="0x2C9" end="0x2CB"/>
...
</chars>
<special>
<range start="0x00" end="0x1F" advance="6"/>
<range start="0x61" end="0x66" color="1"/>
<range start="0x63" end="0x63" color="0"/>
</special>
</font_metadata>
The <chars> tag can be used to specify characters instead of in a .txt.
The original idea was to just always use the existing .txt system for
specifying the font charset, and only use the XML for the other stuff
that the .txt doesn't cover. However, it's probably better to keep it
simple if possible - having to only have a .png and a .fontmeta seems
simpler than having the data spread out over three files. And a major
advantage: Chinese fonts can have about 30000 characters! It's more
efficient to be able to have a tag saying "now there's 20902 characters
starting at U+4E00" than to include them all in a text file and having
to UTF-8 decode every single one of them.
If a font.txt exists, it takes priority over the <chars> tag, and in
that case, there's no reason to include the <chars> tag in the XML.
But font.txt has to be in the same directory as font.png, otherwise it
is rejected. Same for font.fontmeta. If neither font.txt nor <chars>
exist, then the font is seen as a 2.2-and-below-style ASCII font.
In <special>: advance is the number of pixels the cursor advances after
drawing the character (so the width of the character, without affecting
the grid in the source image), color is whether the character should
have its original colors retained when printed (for button glyphs).
As for <white_teeth>:
The renderer PR has replaced draw-time whitening of sprites/etc
(using BlitSurfaceColoured) by load-time whitening of entire images
(using LoadImage with TEX_WHITE as an argument).
This means we have a problem: fonts have always had their glyphs
whitened at printing time, and since I'm adding support for colored
button glyphs, I changed it so glyphs would sometimes not be whitened.
But if we can't whiten at print time, then we'd need to whiten at load
time, and if we whiten the entire font, any colored glyphs will get
destroyed too. If you whiten the image selectively, well, we need more
code to target specific squares in the image, and it's kind of a waste
when you need to whiten 30000 12x12 Chinese characters when you're only
going to need a handful, but you don't know which ones.
The solution: Whitening fonts is useless if all the non-colored glyphs
are already white, so we don't need to do it anyway! However, any
existing fonts that have non-white glyphs (and I know of at least one
level like that) will still need to be whitened. So there is now a
font property <white_teeth> that can be specified in the fontmeta,
which indicates that the font is already pre-whitened. If not
specified, traditional whitening behavior will be used, and the font
cannot use colored glyphs.
The MAKEANDPLAY, NO_CUSTOM_LEVELS, and NO_EDITOR defines remove content
or features. However, they then raise several warnings because of some
cases, functions, or variables that end up not being used.
This silences them by using the UNUSED macro, or by adding a default
catch-all case if the define is defined (so unhandled cases will still
raise warnings in a build that doesn't have these defines).
We get these warnings because of the typedefs for 64-bit integers in
PhysFS's header files. The compiler will treat them as extensions and
will still compile it fine but it does mean we aren't strictly standards
conforming. Which really isn't a problem anyway. Probably.
This adds the build type in brackets in `-version` output after e.g.
"VVVVVV v2.4". The build type is MAKEANDPLAY, NO_CUSTOM_LEVELS, or
NO_EDITOR (which are not necessarily mutually exclusive).
This is appended on to the end of the first line so as to not break
Ved's existing `-version` check which only checks if the beginning of
STDOUT is "VVVVVV" followed by any version number.
This makes it so that whenever the game loads a script as directed by a
script command, it will first try to load the script from the processed
argument, and if that fails only then will it try to load the script
from the raw argument.
This fixes a regression reported by Dav999 in the custom level "Vungeon"
created by Dynaboom, where a script `ifflag`s to `aselectP1.1` even
though the actual script name is `aselectp1.1`. In 2.3, it would
lowercase `aselectP1.1` and load the script properly, but previous to
this commit it would try to load the script with a capital name and then
fail.
This aborts and prints the error from SDL_GetError() if
SDL_CreateWindow() or SDL_CreateRenderer() fails.
We abort because there's not much point in continuing if the window or
renderer can't be created. There might be a use case for running the
game in headless mode, but let's code that in explicitly if we ever want
it.
This sets the minimum window size (to 320 x 240), so that the window
cannot be resized to lower than that.
This is because there's no utility in having a game window smaller than
that, and it provides a bonus convenience of being able to resize the
game to exactly 320x240 without needing to be exactly precise about it.
This idea was suggested by Dav999.
The `point` struct was a relic of ActionScript and was added because of
the Flash 'point' object. However, it seems like Simon Roth didn't
realize that SDL has its own point struct.
With this, `Maths.h` can be un-included from a couple headers, which
exposes the fact that `preloader.cpp` was relying on `Maths.h` being
transitively included from `Graphics.h`.
Dav999 added `#include "Localization.h"` before `#include "KeyPoll.h"`,
when it should go after instead, because the letter L comes after the
letter K in the English alphabet.
Ever since VVVVVV was initially ported to C++ in 2.0, it has used surfaces from SDL. The downside is, that's all software rendering. This commit moves most things off of surfaces, and all into GPU, by using textures and SDL_Renderer.
Pixel-perfect collision has been kept by keeping a copy of sprites as surfaces. There's plans for pixel-perfect collision to use masks instead of reading pixel data directly, but that's out of scope for this commit.
- `graphics.reloadresources()` is now called later in `main`, because textures cannot be created without a renderer.
- This commit also removes a bunch of surface functions which are no longer needed.
- This also recaches target textures in certain places for d3d9.
- graphics.images was converted to a fixed-size array.
- fillbox and fillboxabs use SDL_RenderDrawRect instead of drawing an outline using four filled rectangles
- Update my name in the credits
The pixel border around the map fits to map size normally. However, when
the map is off, it's always the dimensions of the full size map, and the
border didn't reflect that, so if the custom minimap was off, and the
map wasn't the full size, it wouldn't fit correctly.
This bug was introduced in PR #898.
The branch name will be added to the window title if it is an interim
version, e.g. "VVVVVV [master]".
This makes it easier for developers to tell at a glance which build of
the game they're running.
While the window is initialized with 640x480, the screen settings
defaulted to 320x240, which is a tiny window. The screen settings take
priority over the initialized window, so people with no previous
settings file will get 320x240. This makes it so they get 640x480
instead.
The window is still initialized to 640x480 (constants used for clarity)
just in case there's some weirdness if it's initialized to a potentially
odd resolution from the user's settings file.
This is useful for developers who may have multiple builds of the game
from various different branches and may easily forget which build of the
game is what.
This shows up in the bottom-right corner of the title screen and also
with the `-version` command-line option, and in the status message
printed when building the game.
Unfortunately there needs to be an intermediate surface for proper alpha
color blending to happen via SDL_BlitSurface. The exact SDL blending
logic seems complicated and unclear for me to implement at the moment,
and my attempts kind of failed, so this is just a stopgap measure to at
least get the game rendering how it was before I screwed it up.
This refactors them to not allocate a temporary surface by instead
simply drawing directly to the destination surface.
This means re-implementing the original semantics of SDL_BlitSurface in
them, which is the function signature they (and BlitSurfaceStandard)
were based off of. So now if src_rect or dest_rect are NULL, it
automatically uses a rect of the entirety of the corresponding surface,
and other things like that. And also some other optimizations like
no-opping if the alpha is 0 (because then nothing will be drawn), and
critical checks (not drawing if the destination pixel is out of bounds,
because then otherwise it would wrap around, or at least that's what it
did when I tested it).
This resulted in a bunch of code that would really suck to copy-paste
because then you'd have to remember to update the other copy, so I
refactored them further and put the common code into one function, while
separating the different code (the exact transformation they do) into
different functions that get passed in through function pointers.
In general, "temp" is a bad name because it could mean anything. In this
case the buffer isn't really temporary and it's only used for drawing
the menu with a certain offset, so I made it use a better name. But also
because I'm going to be adding temporary buffers so I don't want the
names to be confused.
Honestly not too sure why we ever wrote the mask handling logic
ourselves instead of using SDL functions. Hell, we even used SDL_MapRGB
for Graphics::getRGB before.
`ct` was used to be a variable that a color was temporarily stored in
before being passed to a draw function. But this is unnecessary and you
might as well just have a temporary of the color directly. I guess this
was the practice used because temporaries were apparently really bad in
Flash.
setcolreal() was added in 2.3 to do basically the same thing (set it
directly from entities' realcol attributes). But it's no longer needed.
Correspondingly, Graphics::setcol has been renamed to Graphics::getcol
and now returns an SDL_Color, and Graphics::huetilesetcol has been
renamed to Graphics::huetilegetcol for the same reason.
Some functions (notably Graphics::drawimagecol and
Graphics::drawhuetile) were relying on the `ct` to be implicitly set and
weren't ever having it passed in directly. They have been corrected
accordingly.
colourTransform is a struct with only one member, a Uint32. The issue
with `Uint32`s is that it requires a bunch of bit shifting logic to edit
the colors. The issue with bit shifting logic is that people have a
tendency to hardcode the shift amounts instead of using the shift amount
variables of the SDL_PixelFormat, which makes it annoying to change the
color masks of surfaces.
This commit fixes both issues by unhardcoding the bit shift amounts in
DrawPixel and ReadPixel, and by axing the `Uint32`s in favor of using
SDL_Color.
According to the SDL_PixelFormat documentation (
https://wiki.libsdl.org/SDL2/SDL_PixelFormat ), the logic to read and
draw to pixels from colors below 32-bit was just wrong. Specifically,
for 8-bit, there's a color palette used instead of some intrinsic color
information stored in the pixel itself. But we shouldn't need that logic
anyways because we don't use colors below 32-bit. So I axed that too.
This makes it so temporary variables have their scopes reduced (if
possible). I also didn't hesitate to fix style issues, such as their
names ("temp" is such a bad name), making them const if possible, and
any code it touched too.
It previously duplicated the for-loop twice, once for tiles.png and
tiles2.png, which just made me sad. Now it doesn't do that.
Also it previously had an alternate tileset == 10 condition for
tiles.png, which didn't seem to do anything because there's no such
thing as tileset 10, and anyways it's useless in-game because when
playing in the actual game it won't draw tiles.png, so I removed it. I
don't know why it was there in the first place.
Since I removed the temp variable from the outer scope, the other usage
of it has to be updated.
For some reason they all have their executable bits set. Presumably this
is because they were made on an NTFS system where every file is
executable (which doesn't sound secure at all but that's another story).
This allows translators to test all text boxes in the scripts. It
doesn't run the scripts themselves - it only shows the basic appearance
of each text box individually, so context may be lost but it's good to
have a way to see any text boxes that might otherwise not be easily
seen because they require specific circumstances to appear.
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.
The android version just got a much needed update to fix some resolution issues on devices with cutouts.
It turns out the mobile source was actually pretty out of date, like 3 versions out of date! This commit brings it up to date.
All the changes have just been about keeping the game running on modern devices, though. The biggest change was adding the Starling library to the project, which made the game GPU powered and sped the whole thing up.
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.
This updates FAudio, LodePNG, and PhysFS. All other submodules do not
have updates.
The most notable is FAudio, which updates to SDL 2.24.0 with many fixes
that, as Ethan put it, you definitely want.
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.
I missed this because to check for all instances of 2.0.22, I did `rg -F
'2.0.22'`. But ripgrep doesn't search in hidden directories by default,
so the actual command to run is `rg -F. '2.0.22'`.
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.
Doesn't hurt to keep things up to date. (The other submodules, UTF-CPP
and LodePNG, haven't been updated since then.) In particular, PhysFS has
worked around a bug with Windows Explorer, so people should no longer
have issues modifying their data.zip with Explorer and then being unable
to have assets in their game (as reported in icculus/physfs#24 ).
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.
This replaces vcpkg with simply downloading the pre-compiled
dependencies from official upstream releases. The rationale is that
vcpkg is sometimes really slow to update to the latest SDL version when
it releases, and also that it requires the runner to compile SDL every
single time it's instantiated, which is slow and wasteful.
Instead, download the pre-compiled binaries of SDL from its release page
on GitHub. This way, we don't have to compile it ourselves, and we
aren't waiting on vcpkg whenever SDL releases a new version. And for
good measure, cache it so we aren't downloading it _every_ time, which
is even more efficient.
The same can't be done for SDL_mixer, though, because it doesn't have a
GitHub release page with pre-compiled binaries. Instead, we'll download
them from libsdl.org, which is an infrastructure that takes more strain
than if we used GitHub instead. But, it shouldn't matter anyways,
because we cahe this too. And we are going to ditch SDL_mixer for FAudio
in 2.4 anyways, so it's a moot point either way.
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.
I have just built a new CentOS container based off of the updated
Dockerfile, which now has SDL 2.0.16 installed. This updates the CI to
point to the new container.
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.
Mentioning 2.3 here is going to be anachronistic when 2.4 exists. And
there's not really any point in bumping this version number every time
the game's version number is bumped as well. So it's best to just remove
it.
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.
If there was absolutely no music playing, and you went to the in-game
options to switch between MMMMMM and PPPPPP, the behavior would be a bit
glitchy.
If you started with PPPPPP, switching once to MMMMMM wouldn't play
anything, but then switching back to PPPPPP would play MMMMMM track 15.
Then switching back to MMMMMM wouldn't do anything, but then switching
back to PPPPPP again would play PPPPPP track 15 - and from there, the
behavior is stable.
If you started with MMMMMM, switching once to PPPPPP would play MMMMMM
track 15. Then switching back to MMMMMM wouldn't do anything, but then
switching back to PPPPPP would play PPPPPP track 15 - and as above, the
behavior is stable after that.
Anyways, the point is, -1 shouldn't be passed to musicclass::play()
unless you want glitchy things. And I'm not patching -1 out of
musicclass::play() itself, because passing negative numbers results in a
useful glitch (that's existed since 2.2) where you can play MMMMMM
tracks while having PPPPPP selected, effectively doubling the amount of
usable music tracks within a custom level; it also seems like the game
does -1 checks elsewhere, so I'm just being consistent with the rest of
the game (although, yes, I am technically single-case patching this).
I ran Include What You Use on the file, and a BUNCH of transitive
includes showed up.
colourTransform is used in the file, so GraphicsUtil.h needs to be
included. libc floor() is used in the file, so math.h needs to be
included (I'm removing this next...). NULL is used, so stddef.h. And
stdlib.h is used because we use rand() directly instead of going through
fRandom(). Speaking of which, we use fRandom(), so Maths.h needs to be
included, too.
So, 2.3 added recoloring one-way tiles to no longer make them be always
yellow. However, custom levels that retexture the one-way tiles might
not want them to be recolored. So, if there are ANY custom assets
mounted, then the one-ways will not be recolored. However, if the XML
has a <onewaycol_override>1</onewaycol_override> tag, then the one-way
will be recolored again anyways.
When I added one-way recoloring, I didn't intend for any custom asset to
disable the recoloring; I only did it because I couldn't find a way to
check if a specific file was customized by the custom level or not.
However, I have figured out how to do so, and so now tiles.png one-way
recolors will only be disabled if there's a custom tiles.png, and
tiles2.png one-way recolors will only be disabled if there's a custom
tiles2.png.
In order to make sure we're not calling PhysFS functions on every single
deltaframe, I've added caching variables, tiles1_mounted and
tiles2_mounted, to Graphics; these get assigned every time
reloadresources() is called.
This function will check if a specific file is a mounted per-level
custom asset, instead of being a variable that's true if ANY file is a
mounted asset.
Now you only have to call one function (and pass it a tile number) to
figure out if you should recolor a one-way tile or not, and you don't
have to copy-paste.
It's only used in FileSystemUtils and never anywhere else, especially
not Graphics. Why is this on Graphics again?
It's now a static variable inside FileSystemUtils. It has also been
renamed to assetDir for consistency with saveDir and levelDir. Also,
it's a C string now, and is no longer an STL string.
There's no need to create an std::string for every single element just
to see if it's a key name.
At least in libstdc++, there's an optimization where std::strings that
are 16 characters or less don't allocate on the heap, and instead use
the internal 16-char buffer directly in the control structure of the
std::string. However, it's not guaranteed that all the element names
we'll get will always be 16 chars or less, and in case the std::string
does end up allocating on the heap, we have no reason for it to allocate
on the heap; so we should just convert these string comparisons to C
strings instead.
This bug is technically NOT a regression - the code responsible for it
has been around since the source release.
However, it hasn't been a problem until Graphic Options and Game Options
were added to the pause screen. Since then, if you opened the pause menu
in Flip Mode, pressing up would move to the menu option below, and
pressing down would move to the menu option above. Notably, left and
right still remain the same.
This is because the map screen input code assumes that the menu options
will be flipped around - however, this has never been the case. What
happens instead is that the menu options get flipped around time when in
Flip Mode - flipping what's already flipped - so it ends up the same
again.
(Incidentally enough, the up/down reversing code is present on the title
screen, and is correct - if you happen to set graphics.flipmode to true
on the title screen, the title screen doesn't negate the flipped menu
options, so pressing up SHOULD be treated like pressing down, and vice
versa. However, in 2.3, it's not really possible to set
graphics.flipmode to true on the title screen without using GDB or
modifying the game. In 2.2 and previous, you can just complete the game
in Flip Mode, and the variable won't be reset; 2.3 cleaned up all exit
paths to the menu to make sure everything got reset.)
This isn't a problem when there's only two options, but since 2.3 adds
two more options to the pause screen, it's pretty noticeable.
Anyway, this is fixed by simply removing the branch of the
graphics.flipmode if-else in mapinput(). The 'else' branch is now the
code that gets executed unconditionally. Don't get confused by the diff;
I decided to unindent in the same commit because it's not that many
lines of code.
This fixes a "root cause" bug (that's existed since 2.2 and below) where
recreated surfaces wouldn't preserve the blend mode of their original
surface.
The surface-level (pun genuinely unintended) bug that this root bug
fixes is the one where there's no background to the room name during the
map menu animation in Flip Mode.
This is because the room name background relies on graphics.backBuffer
being filled with complete black. This is achieved by a call to
ClearSurface() - however, ClearSurface() actually fills it with
transparent black (this is not a regression; in 2.2 and previous, this
was an "inlined" FillRect(backBuffer, 0x00000000)). This would be okay,
and indeed the room name background renders fine in unflipped mode - but
it suddenly breaks in Flip Mode.
Why? Because backBuffer gets fed through FlipSurfaceVerticle(), and
FlipSurfaceVerticle() creates a temporary surface with the same
dimensions and color masks as backBuffer - it, however, does NOT create
it with the same blend mode, and kind of sort of just forgets that the
original was SDL_BLENDMODE_NONE; the new surface is SDL_BLENDMODE_BLEND.
Thus, transparency applies on the new surface, and instead of the room
name being drawn against black, it gets drawn against transparency.
Here I'm using "surface recreation" to mean allocating a new surface
with almost the exact same properties as a given previous. As you can
see, GraphicsUtil likes to recreate surfaces all the time - copying the
masks and flags (unused lol) of an existing surface - and only varies it
by the dimensions of the new surface.
As you can see, this is a lot less wordy and a lot less repetitive than
copy-pasting it a bunch.
In normal mode, the room name is at the bottom of the screen. When you
bring up the map screen, it appears as if the room name is moving up
from the bottom of the screen, and the map screen is "pushing" it up.
The effect is pretty seamless, and when I first played the game (back in
2014), I thought it was pretty cool.
However, in Flip Mode, the room name is at the top of the screen. So one
would expect the menu animation to come from above the screen. Well, no,
it still goes from the bottom of screen; ruining the effect because it
seems like there are two room names on the screen, when there ought to
be only one.
To be fair, I only noticed this while fixing another bug now, but it's
one of those things you can't unsee (I have cursed you with knowledge!);
not to mention that I probably only didn't notice this because I don't
play in Flip Mode that often (and I'd wager almost no one does; Flip
Mode previous to 2.3 seems to have been really untested, like I said
in #165). It feels like a bit of an oversight that the direction of the
animation is the same direction as in unflipped mode. So I'm fixing
this.
If you stood in two activity zones at once, you'll automatically select
the one that got created first. And when you activated it, the activity
zone prompt would switch to fading out the prompt of the OTHER activity
zone, the one you didn't activate.
This wasn't a problem in 2.2 and previous, because the fading animation
was simply bugged and defaulted to being solid black. However, in 2.3,
the fading animation is fixed, so this is possible.
Also, this really only happens in the main game. Since there's only one
type of useful activity zone in custom levels - namely the terminal
activity zone - if two activity zones did happen to overlap, activating
one of them wouldn't result in visibly fading out a different activity
zone (because they both look the same); furthermore custom level makers
are careful to not overlap terminal activity zones, lest this result in
player confusion; furthermore the placed activity zones only cover a
small area, whereas in the main game, crewmates' activity zones are
pretty big.
(Technically, you CAN create main game activity zones in custom levels,
but those are hardcoded to call main game scripts, and basically nobody
uses them.)
So what's the solution? Simply adding game.hascontrol and script.running
checks to the updating of game.activity_last[prompt|r|g|b].
Why not add those checks to the assignment of game.activeactivity, just
above? Because that would introduce a frame ordering issue (that
would NOT be (automatically) fixed by #535) where the eligibility of
pressing Enter on an activity zone now checks if you were standing in an
activity zone LAST frame, and not THIS frame. (I tested this with
libTAS.) Better to fiddle with the rendering code than fiddle with the
actual physics code.
The specific spot I used to test this was standing in Violet's activity
zone and the activity zone of the ship radio terminals (the three
terminals on the ground in her room); the ship radio terminals are
first-placed, so if you're testing this (and you should!), make that the
prompt is of the ship radio activity zone before activation.
This probably should've been moved to RenderFixed a while ago, because
it's unnecessary to run this on every single deltaframe.
The only minor wrinkle here is that this means rendering of activity
zone fades will be delayed for 1 frame, but #535 will fix that.
Since you're now allowed to bring up the map screen during cutscenes,
you've also been able to activate activity zones and teleporter prompts
during cutscenes. This only really affects custom levels; nowhere in the
main game can you overlap with an activity zone while in a cutscene.
To fix this, I've just added a script.running check to Enter keybind
processing.
I was looking through all calls to game.returnmenu(), and I noticed that
the return option in the game pad screen didn't have a
map.nexttowercolour(). I tested it and, yep, returning from there
doesn't update the background color.
So that should be fixed now.
I'm... not sure why this was here? It's absolutely not needed.
I'm guessing maybe at one point during development, there might have
been wanted a special song to be played during the credits, or no song
at all (although the function being niceplay() instead of play() seems
to support the first possibility) - but there's no need for this to be
here.
Now that recreating the same menu keeps currentmenuoption, we can remove
all these superfluous assignments. This means repeating ourselves less;
in case the option numbers change in the future, we won't have to
remember to update these reassignments, too.
When recreating the same menu, there's basically no reason to reset the
currently-selected menu option. (Also, no need to worry about indexing
out of bounds or anything - the number gets checked while iterating over
all menu options; it's never used to actually index anything. At worst
there might be a 1-frame flicker as the bounds code in gameinput() kicks
in, but that shouldn't happen anyways.)
Zip files that have been successfully mounted in editorclass::loadZips()
will now be ignored when the game does its second pass over the levels
directory. Otherwise, this would produce a superfluous error message,
because the game would attempt to parse the zip file as a level file
(when it's not a level file and is in fact a binary file).
This returns if the file given is mounted or not. 2.3 added level zip
support, so whenever the game loads level metadata, it will mount any
zip files in the levels directory; this function can be used to check if
any of those files have been mounted, and ignore them if so.
Otherwise, this would produce a superfluous warning message in the
console. Directories are now ignored and never attempted to be opened;
so now any warning messages printed out are genuine file that something
has genuinely gone wrong with.
Well, there's still a warning message printed if there's a symlink to a
directory; this is rarer, but it's still a false positive.
This function will be used to differentiate files from directories.
Or at least that was the hope. Symlink support was added in 2.3, but it
doesn't seem like PHYSFS_stat() lets you follow the symlink to check if
what it points to is itself a file or directory. And there doesn't seem
to be any function to follow the symlink yourself...
So for now, this function considers symlinks to directories to be files.
PHYSFS_readBytes() returns a PHYSFS_sint64, but we forcefully shove it
into a 32-bit signed integer.
Fixing the type of this doesn't have any immediate consequences, but
it's good for the future in case we want to use the return value for
files bigger than 2 gigabytes; it doesn't harm us in any way, and it's
just better housekeeping.
PHYSFS_fileLength() returns -1 if the file size can't be determined. I'm
going to set it to 0 instead, because it seems like that's more
well-behaved with consumers.
Take lodepng_decode24() or lodepng_decode32(), for example - from a
quick glance at the source, it only takes in a size_t (an unsigned
integer) for the filesize, and one of the first things it does is malloc
with the given filesize. If the -1 turns into SIZE_MAX and LodePNG
attempts to allocate that many bytes... well, I don't know of any
systems that have 18 exabytes of memory. So that seems pretty bad.
The function returns a PHYSFS_sint64, but we forcefully shove it into a
PHYSFS_uint32. This means we throw away all the negative numbers, which
is bad because the function returns -1 if the size of the file can't be
determined; plus, we also throw away 32 bits of information, reducing
our range of supported file sizes from 9 exabytes to 4 gigabytes.
File size support is only as good as the weakeast link, and it looks
like one of the consumers of FILESYSTEM_loadFileToMemory(),
SDL_RWFromConstMem(), only takes in a signed 32-bit integer of size;
however, I would still like to do at least the bare minimum to support
as many file sizes as we can, and changing types around is one of those
bare minimums.
I had misread this line in #629 and thought that it was just clearing
the entire surface, when really it was filling the surface with opaque
black. ClearSurface() would instead make it transparent, which would
mean when it got drawn, it would get drawn against blue, and not black.
Whoops.
These float attributes are assigned to, and then never read again. The
coordinate systems of blocks are a bit of a mess - some use xp/yp, some
use xp/yp and rect.x/rect.y - but I can confidently say that these are
never used, because it compiles fine if I remove the attributes from the
class, plus remove all assignments to it.
Screen.cpp wasn't explicitly including SDL.h, instead relying on
Screen.h to include it.
It was also relying on SDL.h to include stdio.h on Linux, which breaks
because SDL.h doesn't include stdio.h on Windows. So stdio.h is now
explicitly included as well.
stdlib.h is not used in this file.
After reasoning about it for a bit, there's no reason for these checks
to be here. `zip_normal` will either be
/home/infoteddy/.local/share/VVVVVV/levels if the asset directory is a
directory, or levels/levelname.zip if the asset directory is inside the
same zip as the level is. I don't see how they could ever be data.zip.
My guess is because of the VCE bug where it messed up its search path,
and before that bug was fixed, it had to be worked around here by
explicitly blacklisting data.zip here. When the assets mounting stuff
was ported from VCE to vanilla, vanilla didn't have the problem, and so
this data.zip blacklisting stuff was unnecessary.
Either way, I see no reason for this, so I'm going to remove it.
There is no need to use heap-allocated strings here, so I've refactored
them out. I've also cleaned up both of the functions a bit, because the
line spacing of the previous version was completely non-existent, brace
style was same-line instead of next-line, and the variable names were a
bit misleading (in FILESYSTEM_mountassets(), there is a `zippath` AND a
`zip_path`, which are two completely different variables).
Also, FILESYSTEM_mount() now prints an error message and bails if
PHYSFS_getRealDir() returns NULL, whereas it didn't do that before.
The function is literally just an alias for PHYSFS_exists(), which does
not exclusively check for directories. Plus, the function is also used
to check if a non-directory file exists. Why is this function named
"directoryExists"?!
The info message when a .data.zip file is mounted is now differentiated
from the message when an actual directory is mounted (the .data.zip
message specifies ".data.zip").
The error message for an error occurring when loading or mounting a .zip
is now capitalized.
The "Custom asset directory does not exist" now uses puts(), because
there's no need to use printf() here.
I don't know why this is here; it's unused. I don't know why the
compiler doesn't warn about this being unused either - maybe it's
secretly being used? That also means I'm not sure if the compiler is
optimizing this away or not. Anyway, this is getting removed.
The STL here cannot be completely eliminated (because the custom entity
object uses std::string), but at least we can avoid unnecessarily making
std::strings until the very end.
There's not really any reason for this function to use heap-allocated
strings. So I've refactored it to not do that.
I would've used SDL_strrstr(), if it existed. It does not appear to
exist. But that's okay.
PhysFS by default just uses system malloc(), realloc(), and free(); it
provides a way to change them, with a struct named PHYSFS_Allocator and
a function named PHYSFS_setAllocator().
According to PhysFS docs, this function should be called before
PHYSFS_init(), which is why this allocator stuff is handled in
FileSystemUtils.cpp.
Also, I've had to make two "bridge" functions, because PHYSFS_Allocator
wants pointers to functions taking in `PHYSFS_uint64`s, not `size_t`s.
ClearSurface() is less verbose than doing it the old way, and also
conveys intent clearer. Plus, some of these FillRect()s had hardcoded
width and height values, whereas ClearSurface() doesn't - meaning this
change also has better future-proofing, in case the widths and heights
of the surfaces involved change in the future.
When you pass NULL in for the SDL_Rect* parameter to SDL_FillRect(), SDL
will automatically fill the entire surface with that color. There's no
need for us to create the SDL_Rect ourselves.
This is a function that does what it says - it clears the given surface.
This just means doing a FillRect(), but it's better to use this function
because it conveys intent better.
This is pretty old commented-out code from earlier versions of the game;
they are no longer useful, and are just distracting. If we need them, we
can always refer back to this commit (but I sincerely doubt that we'll
need them).
Apparently in C, if you have `void test();`, it's completely okay to do
`test(2);`. The function will take in the argument, but just discard it
and throw it away. It's like a trash can, and a rude one at that. If you
declare it like `void test(void);`, this is prevented.
This is not a problem in C++ - doing `void test();` and `test(2);` is
guaranteed to result in a compile error (this also means that right now,
at least in all `.cpp` files, nobody is ever calling a void parameter
function with arguments and having their arguments be thrown away).
However, we may not be using C++ in the future, so I just want to lay
down the precedent that if a function takes in no arguments, you must
explicitly declare it as such.
I would've added `-Wstrict-prototypes`, but it produces an annoying
warning message saying it doesn't work in C++ mode if you're compiling
in C++ mode. So it can be added later.
One of these days, I need to get around to running Include What You Use
on this codebase. Until then, while I was working on #624, I noticed
these; I'm removing them now.
The recently released SDL 2.0.14 adds a native function for opening URIs
from the host system, superseding the OS-specific implementations of
FILESYSTEM_openDirectory.
This fixes a regression where moving platforms had no collision. Because
their width and height would be maintained, but their type would be -1.
(Also because I didn't test enough.)
In #565, I decided to set blocks' types to -1 when disabling them, to be
a bit safer in case there was some code that used block types but not
their width and heights. However, this means that when blocks get
disabled and re-created in the platform update loops, their types get
set to -1, which effectively also disables their collision.
In the end, I'll just have to compromise and remove setting blocks to
type -1. Because in a better world, we shouldn't be destroying and
creating blocks constantly just to move some platforms - however, fixing
such a fundamental problem is beyond the scope of at least 2.3 (there's
also the fact that this problem also results in some bugs that are a
part of compatibility, whether we like it or not). So I'll just remove
the -1.
next_split_s() could potentially commit out-of-bounds indexing if the
amount of source data was bigger than the destination data.
This is because the size of the source data passed in includes the null
terminator, so if 1 byte is not subtracted from it, then after it passes
through the VVV_min(), it will index 1 past the end of the passed buffer
when null-terminating it.
In contrast, the other argument of the VVV_min() does not need 1
subtracted from it, because that length does not include a null
terminator (next_split() returns the length of the substring, after all;
not the length of the substring plus 1).
(The VVV_min() here also shortens the range of values to the size of an
int, but we'll probably make size_t versions anyway; plus who really
cares about supporting massively-sized buffers bigger than 2 billion
bytes in length? That just doesn't make sense.)
If PHYSFS_enumerate() isn't successful, we now print that it wasn't
successful, and print the PhysFS error message. (We should get that
logging thing going sometime...)
Note that level dir listing still uses plenty of STL (including the end
product - the `LevelMetaData` struct - which, for the purposes of 2.3,
is okay enough (2.4 should remove STL usage entirely)); it's just that
the initial act of iterating over the levels directory no longer takes
four or SIX(!!!) heap allocations (not counting reallocations and other
heap allocations this patch does not remove), and no longer does any
data marshalling.
Like text splitting, and binary blob extra indice grabbing, the current
approach that FILESYSTEM_getLevelDirFileNames() uses is a temporary
std::vector of std::strings as a middleman to store all the filenames,
and the game iterates over that std::vector to grab each level metadata.
Except, it's even worse in this case, because PHYSFS_enumerateFiles()
ALREADY does a heap allocation. Oh, and
FILESYSTEM_getLevelDirFileNames() gets called two or three times. Yeah,
let me explain:
1. FILESYSTEM_getLevelDirFileNames() calls PHYSFS_enumerateFiles().
2. PHYSFS_enumerateFiles() allocates an array of pointers to arrays of
chars on the heap. For each filename, it will:
a. Allocate an array of chars for the filename.
b. Reallocate the array of pointers to add the pointer to the above
char array.
(In this step, it also inserts the filename in alphabetically -
without any further allocations, as far as I know - but this is a
COMPLETELY unnecessary step, because we are going to sort the list
of levels by ourselves via the metadata title in the end anyways.)
3. FILESYSTEM_getLevelDirFileNames() iterates over the PhysFS list, and
allocates an std::vector on the heap to shove the list into. Then,
for each filename, it will:
a. Allocate an std::string, initialized to "levels/".
b. Append the filename to the std::string above. This will most
likely require a re-allocation.
c. Duplicate the std::string - which requires allocating more memory
again - to put it into the std::vector.
(Compared to the PhysFS list above, the std::vector does less
reallocations; it however will still end up reallocating a certain
amount of times in the end.)
4. FILESYSTEM_getLevelDirFileNames() will free the PhysFS list.
5. Then to get the std::vector<std::string> back to the caller, we end
up having to reallocate the std::vector again - reallocating every
single std::string inside it, too - to give it back to the caller.
And to top it all off, FILESYSTEM_getLevelDirFileNames() is guaranteed
to either be called two times, or three times. This is because
editorclass::getDirectoryData() will call editorclass::loadZips(), which
will unconditionally call FILESYSTEM_getLevelDirFileNames(), then call
it AGAIN if a zip was found. Then once the function returns,
getDirectoryData() will still unconditionally call
FILESYSTEM_getLevelDirFileNames(). This smells like someone bolting
something on without regard for the whole picture of the system, but
whatever; I can clean up their mess just fine.
So, what do I do about this? Well, just like I did with text splitting
and binary blob extras, make the final for-loop - the one that does the
actual metadata parsing - more immediate.
So how do I do that? Well, PhysFS has a function named
PHYSFS_enumerate(). PHYSFS_enumerateFiles(), in fact, uses this function
internally, and is basically just a wrapper with some allocation and
alphabetization.
PHYSFS_enumerate() takes in a pointer to a function, which it will call
for every single entry that it iterates over. It also lets you pass in
another arbitrary pointer that it leaves alone, which I use to pass
through a function pointer that is the actual callback.
So to clarify, there are two callbacks - one callback is passed through
into another callback that gets passed through to PHYSFS_enumerate().
The callback that gets passed to PHYSFS_enumerate() is always the same,
but the callback that gets passed through the callback can be different
(if you look at the calling code, you can see that one caller passes
through a normal level metadata callback; the other passes through a zip
file callback).
Furthermore, I've also cleaned it up so that if editorclass::loadZips()
finds a zip file, it won't iterate over all the files in the levels
directory a third time. Instead, the level directory only gets iterated
over twice - once to check for zips, and another to load every level
plus all zips; the second time is when all the heap allocations happen.
And with that, level list loading now uses less STL templated stuff and
much less heap allocations.
Also, ed.directoryList basically has no reason to exist other than being
a temporary std::vector, so I've removed it. This further decreases
memory usage, depending on how many levels you have in your levels
folder (I know that I usually have a lot and don't really ever clean it
up, lol).
Lastly, in the callback passed to PhysFS, `builtLocation` is actually no
longer hardcoded to just the `levels` directory, since instead we now
use the `origdir` variable that PhysFS passes us. So that's good, too.
If PHYSFS_mountHandle() failed to mount a zip file, we would print
PhysFS's error message straight, without any surrounding context. This
seems a little weird, and doesn't maximize understanding for readers;
I've made it so now the error message is "Could not mount <zip file>:
<PhysFS error>".
When Ethan added PhysFS to the game, he put in a hardcoded check (marked
with a FIXME) that explicitly removed all filenames that were "data"
returned by PHYSFS_enumerateFiles(). Apparently this was due to a weird
bug with the function putting in "data" strings in its output in PhysFS
2.0.3; however, the game now uses PhysFS 3.0.2, and I could not
reproduce this bug on my system. (I also tested, and this also
straight-up ignores legitimate level filenames that just happen to be
"data" (without the .vvvvvv extension).)
After talking with Ethan in Discord DMs, I asked if we could remove this
check, and he said that we could. So I'm doing it now.
Just like I refactored text splitting to no longer use std::vectors,
std::strings, or temporary heap allocations, decreasing memory usage and
improving performance; there's no reason to use a temporary
heap-allocated std::vector to grab all extra binary blob indices, when
instead the iteration can just be more immediate.
Instead, what I've done is replaced binaryBlob::getExtra() with
binaryBlob::nextExtra(), which takes in a pointer to an index variable,
and will increment the index variable until it reaches an extra track.
After the caller processes the extra track, it is the caller's
responsibility to increment the variable again before passing it back to
getExtra().
This avoids all heap allocations and brings down the memory usage of
processing extra tracks.
If you configure the build with -DBUNDLE_DEPENDENCIES=OFF, then VVVVVV
will dynamically link with TinyXML-2 and PhysicsFS instead of using the
bundled source code in third_party/ and statically linking with them.
Unfortunately, it doesn't seem like distros package LodePNG, and LodePNG
isn't intended to be packaged, so we can't dynamically link with it, nor
can we use some system LodePNG header files somewhere else because those
don't exist.
UTF8-CPP is a special case, because no matter what, it's going to be
statically linked with the binary (it doesn't come as a shared object
file in any way). So with -DBUNDLE_DEPENDENCIES=OFF, we will use the
system UTF8-CPP header files instead of the bundled ones, but it will
still be statically linked in with the binary.
The main motivation for doing this is so if VVVVVV ever gets packaged in
distros, distro maintainers would be more likely to accept it if there
was an option to compile the game without bundled dependencies. Also, it
discourages modifying the third-party dependencies we have, because it's
always possible for someone to compile those dependencies without our
changes, with this CMake option.
This patch restores some 2.2 behavior, fixing a regression caused by the
refactor of properly using std::vectors.
In 2.2, the game allocated 200 items in obj.entities, but used a system
where each entity had an `active` attribute to signify if the entity
actually existed or not. When dealing with entities, you would have to
check this `active` flag, or else you'd be dealing with an entity that
didn't actually exist. (By the way, what I'm saying applies to blocks
and obj.blocks as well, except for some small differing details like the
game allocating 500 block slots versus obj.entities's 200.)
As a consequence, the game had to use a separate tracking variable,
obj.nentity, because obj.entities.size() would just report 200, instead
of the actual amount of entities. Needless to say, having to check for
`active` and use `obj.nentity` is a bit error-prone, and it's messier
than simply using the std::vector the way it was intended. Also, this
resulted in a hard limit of 200 entities, which custom level makers ran
into surprisingly quite often.
2.3 comes along, and removes the whole system. Now, std::vectors are
properly being used, and obj.entities.size() reports the actual number
of entities in the vector; you no longer have to check for `active` when
dealing with entities of any sort.
But there was one previous behavior of 2.2 that this system kind of
forgets about - namely, the ability to have holes in between entities.
You see, when an entity got disabled in 2.2 (which just meant turning
its `active` off), the indices of all other entities stayed the same;
the indice of the entity that got disabled stays there as a hole in the
array. But when an entity gets removed in 2.3 (previous to this patch),
the indices of every entity afterwards in the array get shifted down by
one. std::vector isn't really meant to be able to contain holes.
Do the indices of entities and blocks matter? Yes; they determine the
order in which entities and blocks get evaluated (the highest indice
gets evaluated first), and I had to fix some block evaluation order
stuff in previous PRs.
And in the case of entities, they matter hugely when using the
recently-discovered Arbitrary Entity Manipulation glitch (where crewmate
script commands are used on arbitrary entities by setting the `i`
attribute of `scriptclass` and passing invalid crewmate identifiers to
the commands). If you use Arbitrary Entity Manipulation after destroying
some entities, there is a chance that your script won't work between 2.2
and 2.3.
The indices also still determine the rendering order of entities
(highest indice gets drawn first, which means lowest indice gets drawn
in front of other entities). As an example: let's say we have the player
at 0, a gravity line at 1, and a checkpoint at 2; then we destroy the
gravity line and create a crewmate (let's do Violet).
If we're able to have holes, then after removing the gravity line, none
of the other indices shift. Then Violet will be created at indice 1, and
will be drawn in front of the checkpoint.
But if we can't have holes, then removing the gravity line results in
the indice of the checkpoint shifting down to indice 1. Then Violet is
created at indice 2, and gets drawn behind the checkpoint! This is a
clear illustration of changing the behavior that existed in 2.2.
However, I also don't want to go back to the `active` system of having
to check an attribute before operating on an entity. So... what do we
do to restore the holes?
Well, we don't need to have an `active` attribute, or modify any
existing code that operates on entities. Instead, we can just set the
attributes of the entities so that they naturally get ignored by
everything that comes into contact with it. For entities, we set their
invis to true, and their size, type, and rule to -1 (the game never uses
a size, type, or rule of -1 anywhere); for blocks, we set their type to
-1, and their width and height to 0.
obj.entities.size() will no longer necessarily equal the amount of
entities in the room; rather, it will be the amount of entity SLOTS that
have been allocated. But nothing that uses obj.entities.size() needs to
actually know the amount of entities; it's mostly used for iterating
over every entity in the vector.
Excess entity slots get cleaned up upon every call of
mapclass::gotoroom(), which will now deallocate entity slots starting
from the end until it hits a player, at which point it will switch to
disabling entity slots instead of removing them entirely.
The entclass::clear() and blockclass::clear() functions have been
restored because we need to call their initialization functions when
reusing a block/entity slot; it's possible to create an entity with an
invalid type number (it creates a glitchy Viridian), and without calling
the initialization function again, it would simply not create anything.
After this patch is applied, entity and block indices will be restored
to how they behaved in 2.2.
Just like is_positive_num(), an empty string is not a number.
I've also decided to unroll iteration 0 of the loop here so readability
is improved; this happens to also knock out the whole "accepting empty
string" thing, too.
To account for empty strings, we simply have to special-case them.
Simple as that.
This was also a problem with the previous std::string implementation of
this function; regardless, this is fixed now.
The current way "arrays" from XML files are loaded (before this commit
is applied) goes something like this:
1. Read the buffer of the contents of the tag using TinyXML-2.
2. Allocate a buffer on the heap of the same size, and copy the
existing buffer to it. (This is what the statement `std::string
TextString = pText;` does.)
3. For each delimiter in the heap-allocated buffer...
a. Allocate another buffer on the heap, and copy the characters from
the previous delimiter to the delimiter you just hit.
b. Then allocate the buffer AGAIN, to copy it into an std::vector.
4. Then re-allocate every single buffer YET AGAIN, because you need to
make a copy of the std::vector in split() to return it to the caller.
As you can see, the existing way uses a lot of memory allocations and
data marshalling, just to split some text.
The problem here is mostly making a temporary std::vector of split text,
before doing any actual useful work (most likely, putting it into an
array or ANOTHER std::vector - if the latter, then that's yet another
memory allocation on top of the memory allocation you already did; this
memory allocation is unavoidable, unlike the ones mentioned earlier,
which should be removed).
So I noticed that since we're iterating over the entire string once
(just to shove its contents into a temporary std::vector), and then
basically iterating over it again - why can't the whole thing just be
more immediate, and just be iterated over once?
So that's what I've done here. I've axed the split() function (both of
them, actually), and made next_split() and next_split_s().
next_split() will take an existing string and a starting index, and it
will find the next occurrence of the given delimiter in the string. Once
it does so, it will return the length from the previous starting index,
and modify your starting index as well. The price for immediateness is
that you're supposed to handle keeping the index of the previous
starting index around in order to be able to use the function; updating
it after each iteration is also your responsibility.
(By the way, next_split() doesn't use SDL_strchr(), because we can't get
the length of the substring for the last substring. We could handle this
special case specifically, but it'd be uglier; it also introduces
iterating over the last substring twice, when we only need to do it
once.)
next_split_s() does the same thing as next_split(), except it will copy
the resulting substring into a buffer that you provide (along with its
size). Useful if you don't particularly care about the length of the
substring.
All callers have been updated accordingly. This new system does not make
ANY heap allocations at all; at worst, it allocates a temporary buffer
on the stack, but that's only if you use next_split_s(); plus, it'd be a
fixed-size buffer, and stack allocations are negligible anyway.
This improves performance when loading any sort of XML file, especially
loading custom levels - which, on my system at least, I can noticeably
tell (there's less of a freeze when I load in to a custom level with
lots of scripts). It also decreases memory usage, because the heap isn't
being used just to iterate over some delimiters when XML files are
loaded.
These comments were probably remnants of some late-night coding session
or something. Anyway, they're not needed; there's nothing to do with SDL
here, and the "Init" is obvious because the function is a constructor.
Contents and scripts should be reset in editorclass::reset(); there's no
reason to reset them again right before you load them from an XML file
in editorclass::load().
Additionally, the resets now consistently use SDL_zeroa() (for contents)
and scriptclass::clearcustom() (for scripts).
I'm partial to slash-asterisk-style comments, so I'll use those here.
Also, having a space after the start of comments is good. I've also
removed the "Add the script if we have a preceding header" comments
since it can be inferred by reading the surrounding code.
Instead of checking the length() of an std::string, just check if
pText[0] is equal to '\0'.
This will have to be done anyway, because I'm going to get rid of the
std::string allocation here, and I noticed this inefficiency in the
indentation, so I'm going to remove it.
The actual unindent will be done in the next commit.
This now means every XML array loading is done with common,
re-duplicated code. The only exceptions to this are special cases other
than the the majority of cases; the majority being a simple matter of
reading an array of integers and putting it into another array.
Seems like the only reason I hadn't caught the <customlevelscore> tag
until now was because I was focused on de-duplicating all the array
loads in Game::loadstats() and below, forgetting about
Game::loadcustomlevelstats().
In order to be able to use the LOAD_ARRAY() and LOAD_ARRAY_RENAME()
macros in Game::loadcustomlevelstats(), they have to be moved to earlier
in the file.
Even if split() didn't use the STL, using this function here is a bit
unnecessary, because a simple SDL_strchr() suffices. Refactoring split()
to not use the STL will break this caller anyway, so I might as well
just refactor this to not use split() in the first place.
This refactor also properly checks if the inputs are valid integers. And
since split() is no longer used, it also rejects inputs ending with a
trailing comma as being invalid, too; this didn't happen previously.
It's intentional that I used is_number() here instead of
is_positive_num(), thus accepting negative numbers; in the future it
might be possible to have negative room coordinates.
Valgrind reported this.
The error here is that the buffer here is only guaranteed to be
initialized up until (and including) the null-terminator, by
SDL_snprintf(). Iterating over the entire allocated buffer is bad and I
should feel bad as the girl who wrote this code; doing that reads
uninitialized memory and passes it to SDL_tolower().
As a bonus, the iterator increment is now a preincrement instead of a
postincrement.
This fixes memory leaking every single time a file gets loaded(!) when
the list of custom levels gets loaded(!!!), which Valgrind reports. This
memory leak is completely my bad; 2.2 properly frees the loaded file,
and VCE uses an std::unique_ptr - which I decided to ignore and not
think about why it would be there.
It's safe to do this free after uMem gets copied into std::string;
although, in the future, I *am* thinking about refactoring this function
(and the tag finder function) to not use std::strings, and I'll have to
be careful to make sure that the memory management with the file is
correct when I do so.
This makes the freesrc argument of Mix_LoadMUS_RW() 1 instead of 0. If
the argument is nonzero, then the passed SDL_RWops will be automatically
freed when m_music is freed, too.
I don't know why this was 0 before. Setting it to 1 fixes a memory leak
that Valgrind reports (which turns into an actual leak every time custom
assets are mounted or unmounted).
This adds a check that the pointer passed to
FILESYSTEM_loadFileToMemory() isn't NULL, and if it is, just returns
early in the function, instead of continuing later and producing a
different, slightly-misleading error message.
Previously, it was guarded behind a check for the length, which is... I
guess still perfectly fine behavior, but there's no reason to have a
length check here; FILESYSTEM_freeMemory() uses SDL_free(), which does a
check that the pointer passed is non-NULL (the pointer that is passed
here, despite not being initialized upon declaration, is guaranteed to
be initialized by FILESYSTEM_loadFileToMemory() anyway, so).
Following Ethan's example of bailing (calling VVV_exit()) if
binaryBlob::unPackBinary() couldn't allocate memory, I've searched
through and found every SDL_malloc(), then made sure that if it returned
NULL, the caller would bail (because you can't do much when you're out
of memory).
There should probably be an error message printed when the process is
out of memory, but unPackBinary() doesn't print an error message for
being out of memory, so this can probably be added later. (Also we don't
really have a logging system, I'd like to have something like that added
in first before adding more messages.)
Also, this doesn't account for any allocators used by STL stuff, but
we're working on removing the STL, and allocation failure just results
in an abort anyway, so there's not really a problem there.
Wow, there are a lot of these. All of these exit paths now use
VVV_exit() instead, which attempts to save unlock.vvv and settings.vvv,
and also frees all resources so Valgrind is happy. This is a good thing,
because previously unlock.vvv/settings.vvv wouldn't be written to if we
decided to bail for a given reason.
It should be between the include of the corresponding header file for
the source file (Script.h) and the includes of other local header files
(the files that are specific to this codebase only); this is the
convention that includes in all other source files follow.
However, it seems like I misplaced this, so I'm fixing it now.
This is just a function that calls the cleanup() in main.cpp, as well as
calls exit().
I would have liked to use SDL_ExitProcess() here, because that function
has ifdefs for different runtime environments. But alas, it's an
internal function and isn't exported. Ah well; exit() seems to be fine
anyway.
If there's a resource that doesn't otherwise need to be cleaned up and
is still alive upon program shutdown, then it should go in cleanup().
This cleans up Screen, GraphicsResources, Graphics buffers, Graphics
tiles, and musicclass audio upon program shutdown.
Even we technically don't NEED to clean these resources up ourselves
(the kernel is going to get rid of all of it anyway, else it'd be a
security problem), I'm doing this because otherwise Valgrind will
complain about these, and then it'd be difficult to see which memory
leaks are real and which are just "well this isn't really a leak but you
haven't freed this thing when the process exited, and that's technically
what a memory leak is".
These are all resources whose cleanup functions can be safely called
even if they haven't initialized anything yet.
This isn't a memory leak (not even Valgrind complains), because it gets
properly cleaned up in GraphicsResources::destroy(). Still, it's memory
that is just laying around not being used, and in the name of
deallocating things as soon as you no longer need them, we should
deallocate the base tilesheet images after we split all of them into
tiles.
This reduces the memory cost of all tilesheet images by half, since we
were essentially keeping around duplicates for nothing; this doesn't
really have much of an impact with conventional tilesheet sizes, since
they're usually small enough, but since 2.3 allowed for tilesheet images
of any size, this is a pretty big deal for really big tilesheet images.
It's okay to do this, even though they also get freed in
GraphicsResources::destroy(), because SDL_FreeSurface() does a NULL
check on the pointer passed to it, and we set the pointer to NULL after
freeing the surfaces.
A quick glance at PhysFS source code will show that PhysFS will bail if
PHYSFS_deinit() is called if it's not initialized.
"Bail" here just means setting an error code and returning early, so
it's not that bad. Still, it's the principle of the thing, and I just
want to ensure that FILESYSTEM_deinit() can be safely called no matter
if the filesystem hasn't initialized yet; having an error set by PhysFS
kind of taints the whole safety thing, even if it does nothing wrong,
no?
(although, speaking of which, we should be handling all errors by
PhysFS, but that's for later...)
These FIXME comments are still correct about code duplication, but
they're incorrect about where exactly the original code is after the
original code got moved around. So I've fixed them to refer to the
correct locations.
We really should get around to de-duplicating the code mentioned in
these comments...
Since musicWriteBlob is a temporary object that gets destroyed at the
end of musicclass::init(), in order to make sure we don't leak memory
and lose all the pointers to the blocks we just allocated in
musicWriteBlob, we need to call its clear() method after writing
BinaryMusic.vvv.
musicReadBlob was used for both MMMMMM and PPPPPP soundtracks. This
causes a memory leak if you have mmmmmm.vvv installed, because the
pointers holding each allocated block of MMMMMM would be lost when
PPPPPP got loaded. Valgrind complains about this memory leak.
This is in contrast to 2.2 and previous behavior, where musicReadBlob
was only a temporary object instead of being held in musicclass.
However, this wasn't really a memory leak (moreso something that just
didn't get cleaned up when closing the game), but it did get turned into
a leak when per-level assets mounting and unmounting got introduced in
2.3 (loading a level with custom assets after starting the game with an
mmmmmm.vvv, or exiting out of a level that had an mmmmmm.vvv, would
cause the game to leak memory). Leo recognized this, and moved
musicReadBlob onto musicclass in a separate 2.3 PR, but either he didn't
think about what was happening here too closely, or he didn't use
Valgrind, because he forgot about the memory leak caused by re-using the
same binaryBlob for PPPPPP and MMMMMM.
So instead, just use two different binaryBlob objects for MMMMMM and
PPPPPP. That way, no memory leaks happen.
I'm going to introduce another binaryBlob object in to the mix, and I
want to be able to re-use an existing FOREACH_TRACK #define without
having to copy-paste it again. So, TRACK_NAMES now takes in a blob
parameter, which will be passed to the temporary FOREACH_TRACK #define.
This removes the music cleanup code from musicclass::init(), and
requires that we also call destroy() in Graphics::reloadresources().
This is because we'll need to re-use the musicclass cleanup code
elsewhere, and we don't want to copy-paste the cleanup code. Or at
least, I don't (but I'm not a game dev, game devs copy-paste all the
friggin' time).
It doesn't feel quite write leaving all the buffer creation code in
main(), even though it's perfectly okay to do so and it doesn't result
in any memory mismanagement that Valgrind can report; so I'm factoring
all of it out to a separate function, Graphics::create_buffers().
As a bonus, we no longer have to keep qualifying with `graphics.` in the
buffer creation code, which is nice.
These destroy all the buffers that are created on the Graphics class.
Since these buffers can't be created at the same time as the rest of
Graphics is (due to the fact that they require knowing the pixel format
of the game screen), they can't be destroyed at the same as the rest of
Graphics is, either.
This is a very complicated way of zeroing out grphx (instead of using
SDL_zero()), which itself is completely unnecessary because grphx.init()
gets called immediately afterwards anyway.
It should be next-line brace, not same-line brace. Even in a codebase
that uses same-line braces everywhere, I still prefer having next-line
braces inside functions (because they're at the top level, and you can't
next them). But regardless, this should still be next-line brace like
(most of) the rest of the codebase.
The function previously conditionally freed a m_memblocks pointer if its
corresponding m_headers was valid. This makes me slightly worried about
the possibility that memory would be allocated, but the header would
still be marked as invalid.
I don't see how that could happen, but it's better to be safe than
sorry. SDL_free() does a guaranteed NULL pointer check (like most SDL
functions), so it's okay to pass NULL pointers to it.
Just to be sure, I'm also zeroing m_memblocks and m_headers after
freeing everything in the function.
MSVC complains about these, doesn't seem like GCC does. These can be
safely removed because they're unreachable, and they always follow a
case-switch or similar that has a default case which this code is a
duplicate of anyway. (Unless it isn't, in which case all the better to
remove it, becausee otherwise it looks misleading or confusing to casual
glances at the code.)
find_tag() would commit out-of-bounds indexing if someone made a level
file with malformed XML entity encodings in the metadata tags.
This would happen if the end of the string followed immediately after an
ampersand and hash, or if there wasn't a semicolon ending an XML entity.
Valgrind complains about these, so I've fixed it.
This fixes a bug where "12" gets properly evaluated as 12, but "148"
gets evaluated as 1408. It's because `place` gets multiplied by `radix`
again, so `retval` gets multipled by 100 instead of 10.
There's no reason to have a `place` variable, so I've removed it
entirely. This simplifies the function a little bit.
The previous person who wrote this (a girl named Misa) clearly didn't
understand the reason why you couldn't compare line[line.length()-1]
directly to a string literal. It's because the former is a char, and the
latter is a pointer to a char. Both are ints, so it compiles fine, but
it doesn't do what you want it to.
Why not just make the latter a char instead of a string literal? Well,
because you can, but also I clearly didn't think this through earlier,
so that's why I didn't do it in the first place.
But this is fixed now.
This avoids an unnecessary copy of the input std::vector, since we don't
need to modify it for anything. This cuts down on unnecessary memory
operations.
Apart from the std::string, this function no longer uses the STL.
ss_toi() is a simple function - it converts the input into an int,
taking as many digits as possible until it reaches a non-digit
character, at which point it stops. It's trivial to implement this
without the STL.
I could've used Int() here, but that would've required copying the
string to a temporary buffer to insert a null-terminator (we can't just
use a pointer-and-length data type either, the string functions don't
operate like that - one disadvantage of C strings!). Instead, I decided
to implement my own conversion to int here, because I don't think the
way we humans write our Arabic numerals is going to change anytime soon.
Also, the std::string input is now passed by const reference, instead of
making a copy - cutting down on unnecessary memory operations.
I personally like putting the asterisk with the type, because despite
the language parsing the asterisk as a part of the name, the pointer
part is clearly a part of the return type of the function. Also,
put constness here, to indicate that the input won't be modified inside
the function.
This comment indicates that the function is used by
UtilityClass::GCString(). Which is unnecessary, because the reader can
trivially search for usages of GCChar in the file itself (the 'static'
preceding the function should be a good enough hint) - and if there
aren't any, then the reader will know the function is unused, whereas if
they read the comment, they would have been under the assumption that it
wasn't used. (There might also a compiler warning about it being unused,
which would be more confusing if the comment was still there.)
Point is, comments can get outdated, so removing the comment here makes
the code more self-documenting.
This is a re-do of 942217f871 (#509), but
with a more conservative fix that only resets the player's newxp and
newyp when they respawn from a checkpoint or spawn in to the map.
Unlike the previous patch, if the player were to suddenly collide with a
conveyor or horizontally-moving platform during gameplay, their
y-position would revert back to the intended next y-position of the
previous frame. But this is the same behavior as before, I haven't ever
seen such a contrived situation come up, and this behavior is probably
more preferable for gameplay than actually going to the conveyor, so
it's fine.
I also decided to reset newxp here, and not just newyp, because while
resetting newyp seems to be enough, it's safer to also reset newxp (and
so future readers won't question why only newyp is reset but not newxp).
I tested this and it once again fixes the death loop issue from earlier,
while also still allowing for that Trench Warfare trick to be possible
(I tested it with the libTAS movie I mentioned in #606; it syncs fine).
There are no other known regressions resulting from this fix
(hopefully).
This reverts commit 942217f871.
This fix (of a regression of a fix) has a regression where immediately
flipping off of horizontally-moving platforms or conveyors will no
longer provide you with a "boost" given certain vertical pixel
alignments.
The regression that this fix fixed will be fixed another way.
Fixes#606.
This works on macOS, Wayland, and a few more esoteric platforms. X11
doesn't have a concept of DPI-awareness. Note that with this flag
SDL_GetWindowSize() isn't guaranteed to return the actual window size.
Retextured checkpoints have always been in the game, but clicking on
them in the editor would lead to them losing their retextured-ness. So,
checkpoints should be left alone if their p1 isn't either 0 or 1. Also,
they don't show up properly in the editor, so that's fixed, too.
Retextured and flipped terminals were added in 2.3, and show up properly
in-game, but don't properly show up in the editor, either. So now they
show up in the editor. Additionally, clicking on them will flip the
terminal as well, but only if its p1 is 0 or 1, just like checkpoints
now do.
This call to Makebfont() always existed, but ever since 2.3's per-level
custom assets were added, graphics.reloadresources() also calls
graphics.Makebfont(), meaning this call is unnecessary. Calling it twice
results in graphics.bfont and graphics.flipbfont having twice the number
of elements, until custom assets get mounted (or unmounted, technically).
This does the same thing as the last commit, but for No Death Mode
instead of Time Trials. Whenever you die in No Death Mode, or complete
it, all the relevant variables get copied to variables prefixed with
'ndmresult' that never get reset by script.hardreset(), and these
variables are what titlerender() use, instead of the "live" ones.
This makes it so when a Time Trial gets completed, all the relevant
variables get copied onto variables prefixed with 'timetrialresult',
which never get reset by script.hardreset(). Then titlerender() will use
those variables accordingly.
There are multiple different exit paths to the main menu. In 2.2, they
all had a bunch of copy-pasted code. In 2.3 currently, most of them use
game.quittomenu(), but there are some stragglers that still use
hand-copied code.
This is a bit of a problem, because all exit paths should consistently
have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level
custom assets. Furthermore, most (but not all) of the paths call
script.hardreset() too, and some of the stragglers don't. So there could
be something persisting through to the title screen (like a really long
flash/shake timer) that could only persist if exiting to the title
screen through those paths.
But, actually, it seems like there's a good reason for some of those to
not call script.hardreset() - namely, dying or completing No Death Mode
and completing a Time Trial presents some information onscreen that
would get reset by script.hardreset(), so I'll fix that in a later
commit.
So what I've done for this commit is found every exit path that didn't
already use game.quittomenu(), and made them use game.quittomenu(). As
well, some of them had special handling that existed on top of them
already having a corresponding entry in game.quittomenu() (but the path
would take the special handling because it never did game.quittomenu()),
so I removed that special handling as well (e.g. exiting from a custom
level used returntomenu(Menu::levellist) when quittomenu() already had
that same returntomenu()).
The menu that exiting from the level editor returns to is now handled in
game.quittomenu() as well, where the map.custommode branch now also
checks for map.custommodeforreal. Unfortunately, it seems like entering
the level editor doesn't properly initialize map.custommode, so entering
the level editor now initializes map.custommode, too.
I've also taken the music.play(6) out of game.quittomenu(), because not
all exit paths immediately play Presenting VVVVVV, so all exit paths
that DO immediately play Presenting VVVVVV now have music.play(6)
special-cased for them, which is fine enough for me.
Here is the list of all exit paths to the menu:
- Exiting through the pause menu (without glitchrunner mode)
- Exiting through the pause menu (with glitchrunner mode)
- Completing a custom level
- Completing a Time Trial
- Dying in No Death Mode
- Completing No Death Mode
- Completing an Intermission replay
- Exiting from the level editor
- Completing the main game
Comments in general don't get verified by the compiler, but
commented-out code is even worse. Especially since this looks to be
outdated code.
As always, if we need some of this code, then we can just look back in
the Git history.
This fixes a segfault, because we would then pass compressed image data
to SDL_ConvertSurfaceFormat() in LoadImage(). I didn't test my previous
PR. Whoops.
Implicit conversion warnings happen all over the codebase, but there's
no reason to warn on all of them, and adding casts everywhere is
annoying to read and patently unnecessary.
MSVC is the only compiler that has this warning (GCC even on -Wall
-Wextra doesn't warn about implicit conversions), so disable it for
MSVC.
While compiling in release mode, GCC warns about these two potentially
being used uninitialized further down. The only way this could happen is
if the case-switches below didn't match up with a case, which would
require the game to be in an invalid state (and have invalid values for
rcol and spcol), but it's better to be safe than sorry.
The only thing we need LodePNG for is to decode a PNG that we've already
loaded into memory. We handle the filesystem part ourselves, so we don't
need LodePNG's filesystem functions; we don't encode images, and we
don't use the zlib functions. So disable all of those.
During 2.3 development, there's been a gradual shift to using SDL stdlib
functions instead of libc functions, but there are still some libc
functions (or the same libc function but from the STL) in the code.
Well, this patch replaces all the rest of them in one fell swoop.
SDL's stdlib can replace most of these, but its SDL_min() and SDL_max()
are inadequate - they aren't really functions, they're more like macros
with a nasty penchant for double-evaluation. So I just made my own
VVV_min() and VVV_max() functions and placed them in Maths.h instead,
then replaced all the previous usages of min(), max(), std::min(),
std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max().
Additionally, there's no SDL_isxdigit(), so I just implemented my own
VVV_isxdigit().
SDL has SDL_malloc() and SDL_free(), but they have some refcounting
built in to them, so in order to use them with LodePNG, I have to
replace the malloc() and free() that LodePNG uses. Which isn't too hard,
I did it in a new file called ThirdPartyDeps.c, and LodePNG is now
compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition.
Lastly, I also refactored the awful strcpy() and strcat() usages in
PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save
migration is getting axed in 2.4, but it still bothers me to have
something like that in the codebase otherwise.
Without further ado, here is the full list of functions that the
codebase now uses:
- SDL_strlcpy() instead of strcpy()
- SDL_strlcat() instead of strcat()
- SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above)
- VVV_min() instead of min(), std::min(), or SDL_min()
- VVV_max() instead of max(), std::max(), or SDL_max()
- VVV_isxdigit() instead of isxdigit()
- SDL_strcmp() instead of strcmp()
- SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi()
- SDL_strstr() instead of strstr()
- SDL_strlen() instead of strlen()
- SDL_sscanf() instead of sscanf()
- SDL_getenv() instead of getenv()
- SDL_malloc() instead of malloc() (replacing in LodePNG as well)
- SDL_free() instead of free() (replacing in LodePNG as well)
This patch de-duplicates the tool drawing code a bit in the menu that
gets brought up when you press Space in the level editor, as well as
fixes several bugs related to the fact that the original author(s) of
the code decided to copy-paste everything. (It was most likely Terry,
judging by the distinct lack of whitespace between tokens in the code.)
There are two "pages" of tools that get shown when you open the tool
menu, according to your currently-selected tool.
1. On the first page, your currently-selected tool gets a brighter
outline. However, on the second page, the code to draw the outline over
your currently-selected tool is missing. So I've fixed that.
2. On the first page, the glyph indicator next to the tool icon also
gets brighter when you have that tool selected. However, on the
second page, the code that drew the brighter-colored indicator got
ran before the code that drew the normal-colored indicator, so this
was never shown. This is also fixed.
3. The glyph indicator of the gravity line tool didn't get brighter when
you had it selected, due to its special-cased copy-pasted code
drawing its brighter color before drawing its normal color. This has
also been fixed.
4. Lastly, the tool menu no longer draws the brighter-colored glyphs on
top of the normal-colored glyphs. Instead, the menu will simply draw
the brighter-colored glyphs and will not draw the normal-colored
glyphs in the first place. This is because double-drawing text like
this will look bad if the user has a custom font.png that has
translucent pixels, like I do.
All of these bugs have been fixed by paying off the technical debt of
copy-pasting code.
This variable seems to have been intended to make sure
game.savestatsandsettings() was called at the end of the frame, or make
sure that it didn't get called more than once per frame. I don't see any
frame ordering-related reason why it needs to be called specifically at
the end of the frame (the function doesn't modify any state), so it's
more plausible that it was added to make sure it didn't get called more
than one per frame.
However, upon further analysis, none of the code paths where
game.savemystats is used ever calls or sets game.savemystats more than
once, and a majority of the code directly calls
game.savestatsandsettings() anyway, so there's no reason for this
variable to exist. If we ever need to make sure it doesn't get called
more than once, and there's no way to change the code paths around to
prevent it otherwise, we can use the defer callbacks system that I added
to #535, when it gets merged.
These variables basically serve no purpose. map.customx and map.customy
are clearly never used. map.finalx and map.finaly, on the other hand,
are basically always game.roomx and game.roomy respectively if
map.finalmode is on, and if it's off, then they don't matter.
Also, there are some weird and redundant variable assignments going on
with these; most notably in map.gotoroom(), where rx/ry (local
variables) get assigned to finalx/finaly, then finalx/finaly get
assigned to game.roomx/game.roomy, then finalx/finaly get assigned to
rx/ry. If finalx/finaly made a difference, then there'd be no need to
assign finalx/finaly back to rx/ry. So it makes the code clearer to
remove these weird bits of code.
2.3's per-level assets feature also added a hotkey to reload the custom
assets of the level you're currently editing in the editor, so you
wouldn't have to re-load the level yourself. This hotkey is F9, but
however, it hasn't been documented in the hotkey list brought up by
pressing Shift, until now.
This patch cleans up unnecessary exports from header files (there were
only a few), as well as adds the static keyword to all symbols that
aren't exported and are specific to a file. This helps the linker out in
not doing any unnecessary work, speeding it up and avoiding silent
symbol conflicts (otherwise two symbols with the same name (and
type/signature in C++) would quietly resolve as okay by the linker).
Line clipping and second-frame edge-flipping have been broken since #539
was merged (d910c5118d). The cause of this
is moving the onground/onroof code around.
A proper loop order fix is going to come once #535 gets finalized and
merged, so this is a stopgap measure just to make sure people don't
report that line clipping or second-frame edge-flipping are broken in
current builds of 2.3.
There is a certain ordering of which corners you click on to place enemy
and platform boundaries, and script boxes. You must first click on the
top-left corner, then click on the bottom-right corner. The visual box
that is drawn after you've first clicked on the top-left corner clearly
shows this intended way of doing things.
However, it seems like despite the visuals, the game didn't properly
prevent you from clicking on the corners in the wrong way. If you placed
it from top-right to bottom-left, or bottom-left to top-right, then the
game would place the boxes accordingly, and they would have a weird
shape where two of its opposite sides would just be missing. But,
placing them from bottom-right to top-left is prevented accordingly.
The bug comes down to a simple use of "or", instead of the correct
"and". This isn't the first time the wrong conjunction was used in a
conditional... (8260bb2696, #136)
Since the code block that the if-statement guards is the code that will
execute if the corners placed were correct, the if-statement thus should
be written in the positive case and use a more restrictive "and",
instead of the negative case, which would use a more looser "or". There
are less cases that are correct than cases which are incorrect - in this
case, there is only 1 correct way of doing things (top-left to
bottom-right), compared to 3 incorrect ways of doing things (top-right
to bottom-left, bottom-left to top-right, bottom-right to top-left) - so
when thinking of positive cases, you should be using "and".
Or, you can always just test it. This bug has been in the game since
2.0, so it seems like no one just tested that incorrect input actually
didn't work.
Ever since 2.0, the colors of some of the Time Trial trophies in the
Secret Lab don't correspond to the crewmate of the given level. The
trophy for the Tower uses Victoria's color, and the Lab trophy uses
Vermilion's color. The Space Station 2 trophy uses Viridian's color, and
the Final Level trophy uses Vitellary's color.
This doesn't appear to be intentional, and it would be odd if it was,
since this game matches the colors everywhere else (each zone on the map
is colored with their respective crewmate in mind, for instance). Also,
the Lab trophy has the sad expression, which is Victoria's trait - it
would be weird if this was intended for Vermilion instead.
But the biggest piece of evidence that this was unintentional is the
corresponding comment for each color in Graphics::setcol(). It mislabels
yellow as cyan, cyan as yellow, blue as red, and red as blue.
To fix this, I simply have to set the correct color for each trophy in
case 25 of entityclass::createentity(). I could fix it in
Graphics::setcol() itself, but custom levels might depend on those
certain colors being the way they are, so it's a safer bet to just fix
it in the trophy creation case itself.
The diff of this might look weird. Even though all I'm doing is changing
some value assignments around, it looks like the "patience" algorithm
thinks I'm moving a whole case of the trophy switch-case around.
So... it looks like being able to switch through tilesets backwards has
been in 2.3 for a while, guess no one just uses 2.3 or the level editor
that much. It seems like it's always been broken, too.
If you were on the Space Station tileset (tileset 0), pressing Shift+F1
would keep you on the Space Station tileset instead of switching to the
Ship (tileset 4).
It looks like the problem here was mixing size_t and int together - so
the modulus operation promoted the left-hand side to size_t, which is
unsigned, so the -1 turned into SIZE_MAX, which is 18446744073709551615
on my system. You'll note that that ends in a 5, so the number is
divisible by 5, meaning taking it modulo 5 leads to 0. So the tileset
would be kept at 0.
At least unsigned integer underflow/overflow is properly defined, so
there's no UB here. Just careless type mixing going on.
The solution is to make the modulus an int instead of a size_t. This
introduces an implicit conversion, but I don't care because my compiler
doesn't warn about it, and implicit conversion warnings ought to be
disabled on MSVC anyway.
This fixes a bug where if you entered a tower before watching the
credits sequence, the credits sequence would have mismatched text and
background colors.
This bug happened because entering a tower modified the r/g/b attributes
of mapclass, and updated graphics.towerbg, without updating
graphics.titlebg too. Then gamecompleterender() uses the r/g/b
attributes of mapclass.
The solution is to put the r/g/b attributes on TowerBG instead. That
way, entering a tower will only modify the r/g/b attributes used to
render towers, and won't affect the r/g/b attributes used to render the
credits sequence.
Additionally, I also de-duplicated the case-switch that updated the
r/g/b attributes based off of the current colstate, because it got
copy-pasted twice, leading to three instances of one piece of code.
This fixes a bug where if you completed a custom level during
command-line playtesting, when returning to the title screen, the
background would be red and the text would be white.
This is because playtesting skips over the code path of pressing ACTION
to start the game and advance to the title screen, and the code path of
that ACTION press specifically initializes the title screen colors to
cyan.
This is also caused by the fact that completing a custom level doesn't
call map.nexttowercolour(), but my guess is that the intent there was
that the player would select a custom level, complete it, and return to
the title screen on the same screen with the same colors, so I decided
not to add a map.nexttowercolour() there.
Instead, I've moved the cyan color initialization to main(), so that it
is always executed no matter what, and doesn't require you to take a
specific code path to do it.
With commit 48313169b6 (PR #453),
AllyTally added a single-case patch for a regression, instead of fixing
it at its root cause.
In fact, that commit only fixes the music if Presenting VVVVVV is
playing while exiting to the menu, not if you enter a level that plays
Presenting VVVVVV - so it only fixes it going one way, and not going the
other way around; neither fixing also all the other cases this could
happen.
It doesn't, say, fix the case where you are exited to the menu
automatically after collecting the last crewmate in the level (or if the
level calls gamestate 1013 itself), which is what happens in my MIRA-VIU
TAS video at the end, and which I noted in the description of that video
( https://www.youtube.com/watch?v=OYQO4ePbYW4&t=111 ).
So, the problem here is that when musicclass::play() is called, it sees
that currentsong is the same as its input, and decides that since the
music is already playing, it shouldn't play the music again. Thus, the
music fades out, and we get silence instead of the music playing again.
But I said this was a regression. Why didn't this happen in 2.2? Well,
it's because of the fact that 2.2 sets currentsong to -1 (no music
playing at all) immediately when starting a fadeout, and not when the
fadeout completes (commit facb079b35,
PR #316). As you can imagine, this discrepancy could lead to bugs, given
that the game would think that music wasn't playing when in actuality it
was, but fixing this bug could also break code that expected this wrong
behavior. And in this case, it has.
So to properly fix the root cause of this, instead of naïvely
single-case patching out every case that comes up randomly, in
musicclass::play(), the function will now ignore if the input given is
the same as currentsong if the music is currently fading out.
This reverts commit 48313169b6, "Don't
fade music out when returning to the menu if it's Presenting VVVVVV".
This commit is being reverted because it is only a single-case patch -
that is, it fixes only a single symptom of the bug, and not its
underlying cause.
This is also another conditional where the rest of the function is
nested. Furthermore, in order to not repeat ourselves, I've also decided
to unconditionally assign currentsong to t, because if t is -1
currentsong gets assigned to -1 anyway - so it's the same thing, but
it's much easier to see and think about.
This removes an indentation level, and makes it easier to reason about
the function since you essentially now view it as the function returning
right there.
This prevents issues when calling std::abs with a float on some older
compilers. While it would normally be promoted to an int, std::abs is
special due to being overloaded despite being a C function. This can
cause errors due to the compiler being unable to find a float overload.
SDL_abs doesn't have this problem, since it's a normal C function.
When gamemode(teleporter) gets run in a script, it brings up a read-only
version of the teleporter screen, intended only for displaying rooms on
the minimap.
However, ever since 2.3 allowed bringing up the map screen during
cutscenes (in order to prevent softlocks), bringing up the map screen
during this mode would (1) do an unnecessary animation of suddenly
switching back to the game and bringing up the menu screen again (even
though the menu screen has already been brought up), and (2) would let
you close the menu entirely and go back to GAMEMODE, thus
unintentionally closing the teleporter screen and kind of ruining the
cutscene.
To fix this, when you bring up the map screen, it will instead instantly
transition to the map screen. And when you bring it down, it will also
instantly transition back to the teleporter screen.
But that's not all. The previous behavior was actually kind of a nice
failsafe, in that if you somehow got stuck in a state where a script ran
gamemode(teleporter), but stopped running before it could take you out
of that mode by running gamemode(game), then you could return to
GAMEMODE yourself by bringing up the map screen and then bringing it
back down. So I've made sure to keep that failsafe behavior, only as
long as there isn't a script running.
When bringing up the map screen, the game does a small menu animation
where the menu comes in from the bottom. The code to calculate the menu
offset is copy-pasted everywhere, so I thought I'd de-duplicate it to
make my life easier when working with it. I also included the
game.gamestate assignment in the de-duplicated function, so it would be
easier for a future bugfix.
At the same time, I'm also removing all the BlitSurfaceStandard()s that
copied menubuffer to backBuffer. The red flag is that this blit happened
for every single entry point to MAPMODE and TELEPORTERMODE, except for
the script command gamemode(teleporter). Pressing Enter to bring up the
map screen, pressing Enter to quit the Super Gravitron, pressing Esc to
bring up the pause screen, and pressing Enter to bring up the teleporter
screen all do this blit, so if this blit was there to fix a bug, then
there's a bug with using the script command gamemode(teleporter)... but,
as far as I can tell, there isn't.
That's because the blit basically does nothing. All the blit does is
copy menubuffer onto backBuffer. Then the next thing that happens is
that either maprender() or teleporterrender() will be called, and the
first thing that those functions will always do is fill backBuffer with
solid black, completely overriding the previous blit. So that's why
removing this blit won't have any effect, and it can be safely removed
for code clarity.
When I did #569, I forgot that taking out the code path that set the
player's invis to false meant that the player would still be invisible
upon loading back in to the game if they exited the game while
invisible. Taking out that code path also meant that if game.lifeseq was
nonzero, it wouldn't be reset properly, either. So this fixes those
things.
When I did #567, I didn't test it. And I should have tested it, because
it made the player invisible. This is because map.resetplayer() also
sets the invis attribute of the player to true as well, and I only undid
it setting game.lifeseq to 10.
So instead, I'll just add a flag to map.resetplayer() that by default
doesn't set game.lifeseq or the player's invis attribute. And I tested
it this time, and it works fine. I tested both respawning after death
and exiting to the menu and loading in the game again.
When exiting a level, music.init() gets called again, and every time it
gets called after the first time it gets called, it will free all music
tracks.
To do so, it calls Mix_FreeMusic(). Unfortunately, if there is music
fading, Mix_FreeMusic() will call SDL_Delay(), which will result in
annoying no-draw frames. Meaning, the screen freezes and doesn't draw
anything, and has to wait a bit before continuing.
Here's the relevant piece of code from SDL2_mixer's music.c:
if (music == music_playing) {
/* Wait for any fade out to finish */
while (music->fading == MIX_FADING_OUT) {
Mix_UnlockAudio();
SDL_Delay(100);
Mix_LockAudio();
}
if (music == music_playing) {
music_internal_halt();
}
}
This is especially annoying if you're a TASer, because no-draw frames in
a libTAS movie aren't guaranteed to last for a consistent number of
frames when you change your inputs around.
After this patch, as long as your computer can unmount and re-mount
assets fast enough (it doesn't seem like mine can, unfortunately), then
you won't get any freezes when exiting a level that has custom assets.
(This freeze didn't happen when loading a level because the title screen
music fadeout upon pressing ACTION had enough time to fully complete
before the level got loaded.)
There's a small inconsistency where the first time you load in to the
game, game.lifeseq is at 0, but when you exit to the menu and load in
again, game.lifeseq becomes 10. This is visible as Viridian blinking
when loading in only after the first time you load in, and this also
means that after the first time you load in, you also have to wait 5
frames before being able to move Viridian.
The reason for this inconsistency is because on the first time you load
in to the game, there are no entities loaded in obj.entities yet, so the
game creates a player entity, and doesn't mess with game.lifeseq. When
you exit and then load in for the second time, obj.entities contains at
least one entity (all the entities from the room you just exited out of
- map.gotoroom() hasn't even been called yet, so it doesn't even check
for only the player entity), so the game calls map.resetplayer()
instead, and map.resetplayer() sets game.lifeseq to 10.
There's even an inconsistency to this inconsistency - when you die in No
Death Mode, all entities will be removed from obj.entities, so the next
time you load in to the game, game.lifeseq will be 0.
This inconsistency is pretty minor in the grand scheme of things, but
it still bothers me, so I'm going to fix it.
CI builds were added to this repository on the first day it was
released, and haven't really been touched since then. And since then,
2.3 has added NO_CUSTOM_LEVELS, NO_EDITOR, and OFFICIAL_BUILD builds to
the CMake file.
On top of the MAKEANDPLAY define already existing, this means CI
coverage is a bit sparse - covering compile failures for changes made to
most of the codebase, except for Steam and GOG, and not covering compile
failures if certain parts of the code get stripped out. And people do
forget to check for those configurations as well.
These mess of configurations is kind of a wake-up call to refactor and
generalize the code a bit, because we would probably be able to get rid
of at least two of these (Make & Play, and no-custom-levels) by making
it so custom levels behave indistinguishably from the main game. But,
that's something to do in 2.4. At the very least, we should cover these
in CI right now.
On a small note, I had to add a MAKEANDPLAY configure option to the
CMake file to be able to easily configure a Make & Play build from the
CI runner. This option shouldn't really be used otherwise, so I added a
note to it telling people to consider modifying MakeAndPlay.h instead.
Since INTERIM_COMMIT is a char array whose size we know for sure at
compile time, and which we also know is an array (instead of being a
pointer), we can take the SDL_arraysize() of it. However,
SDL_arraysize() doesn't account for the null terminator unlike
SDL_strlen(), so we'll have to do it ourselves. But at least we are
guaranteed to get a constant value at compile time, unlike if we use
SDL_strlen(), which would be repeatedly evaluating a constant value at
runtime.
To my knowledge, there's no equivalent SDL_arraysize() for constant
strings, and a quick `rg` (ripgrep) for "sizeof" in the SDL include/
folder doesn't show anything like that. So we'll just have to use the
SDL_arraysize() - 1 and deal with it.
The commit hash is now properly updated every time it gets changed, and
not just when CMake gets re-ran.
For this to work, we need to use a few CMake tricks. We add a custom
target with ADD_CUSTOM_TARGET(), which is apparently always considered
out-of-date (but I had to add a BYPRODUCTS line to get it to actually
work), and we use the target to run a .cmake file every time we build.
Also, VVVVVV needs to depend on this custom target, to ensure that the
game gets built AFTER the version gets generated - otherwise there'll be
an error. So we do an ADD_DEPENDENCIES() after the ADD_EXECUTABLE() for
VVVVVV.
This file, version.cmake, is just the Version.h.out generation that I
added previously, but the important thing about all of this is that if
the contents of Version.h.out doesn't change, and thus if the commit
hash hasn't changed, then CMake will never recompile and relink anything
at all. (At least with the Ninja generator.)
On a small note, since the Version.h.out generation is now a separate
script that is guaranteed to get ran on every single build, while the
Git FIND_PACKAGE() gets ran only at configure time, it is possible for
the cached path of the Git executable to get out of date. Fixing this
requires a simple re-configure (ideally), but in case it wasn't fixed,
the INTERIM_COMMIT and COMMIT_DATE variables would get set to empty
strings instead of containing a value. To prevent this from happening,
I've removed ERROR_QUIET from the EXECUTE_PROCESS() calls in
version.cmake, because it's better to explicitly error if the Git
executable wasn't found than implicitly carry on like nothing happened.
The previous implementation of showing the commit hash on the title
screen used a preprocessor definition added at CMake time to pass the
hash and date. This was passed for every file compiled, so if the date
or hash changed, then every file would be recompiled. This is especially
annoying if you're working on the game and switching branches all the
time - the game has at least 50 source files to recompile!
To fix this, we'll switch to using a generated file, named
Version.h.out, that only gets included by the necessary files (which
there is only one of - Render.cpp). It will be autogenerated by CMake
(by using CONFIGURE_FILE(), which takes a templated file and does a
find-and-replace on it, not unlike C macros), and since there's only one
file that includes it, only one file will need to be recompiled when it
changes.
And also to prevent Version.h.out being a required file, it will only be
included if necessary (i.e. OFFICIAL_BUILD is off). Since the C
preprocessor can't ignore non-existing include files and will always
error on them, I wrapped the #include in an #ifdef VERSION_H_EXISTS, and
CMake will add the VERSION_H_OUT_EXISTS define when generating
Version.h.out. The wrapper is named Version.h, so any file
that #includes the commit hash and date should #include Version.h
instead of Version.h.out.
As an added bonus, I've also made it so CMake will print "This is
interim commit [HASH] (committed [DATE])" at configure time if the game
is going to be compiled with an interim commit hash.
Now, there is also the issue that the commit hash change will only be
noticed in the first place if CMake needs to be re-ran for anything, but
that's a less severe issue than requiring recompilation of 50(!) or so
files.
While working on #535, I noticed this bug.
When going to Graphic Options or Game Options from the pause menu,
kludge_ingametemp was intended to save the current menu stack frame
BEFORE either of those menus got created. However, it was actually
assigned afterwards, meaning kludge_ingametemp would always be either
Menu::graphicoptions or Menu::options.
This meant that the returntomenu() in returntopausemenu() would always
attempt to return to the current in-game menu, and seeing as it's the
same menu, would re-create the menu, instead of returning to the
previous menu before it.
This patch also fixes a potential source of a trivial memory leak, if
someone were to keep entering and exiting Graphic Options or Game
Options from the pause menu. It would keep piling up duplicate Graphic
Options or Game Options stack frames, which would never get removed.
However, they do get removed when you exit to the menu properly, by
returntomenu() again, so this doesn't seem like that serious of an
issue, but it's still good to fix.
While I was working on #535, I noticed that all the call sites of
script.run() have the exact same code, namely:
if (script.running)
{
script.run();
}
I figured, why not move the script.running check into the function
itself? That way, we won't have to duplicate the check every single
time, and we don't risk forgetting to add the check and causing a bug
because of that.
The check was already duplicated once since 2.0 (it's used in both
GAMEMODE and TELEPORTERMODE), and with the fix of the two-frame delay in
2.3, it's now duplicated twice, leading to THREE instances of this check
in the code, when there should be only one.
To fix this bug, all we have to do is just pass the existing
ScreenSettings* that we have in loadstats() to savestats(), and in
loadsettings() to savesettings().
Fixes#556. Depends on #558.
Another step to fix the bug #556 is to allow Game::savestats() to accept
a pointer to an existing ScreenSettings struct. This entails refactoring
Game::savesettings() and Game::serializesettings() to accept the
function as well, along with adding Screen::GetSettings() so the
settings of the current Screen can be easily grabbed.
In order to be able to fix the bug #556, I'm planning on adding
ScreenSettings* to the settings.vvv write function. However, that
entails adding another argument to Game::savesettings(), which is going
to be really messy given the default argument of Game::savestats().
That, combined with the fact that the code comment at the site of the
implementation of Game::savestats() being wrong (!!!), leads me to
believe that using default function arguments here isn't worth it.
Instead, what I've done is made it so callers are explicit about whether
or not they're calling savestats(), savesettings(), or both at the same
time. If they are calling both at the same time, then they will be using
a new function named savestatsandsettings().
In short, these are the interface changes:
* bool Game::savestats(bool) has been removed
* bool Game::savestatsandsettings() has been added
* void Game::savestats_menu() has been renamed to
void Game::savestatsandsettings_menu()
* All previous callers of bool Game::savestats() are now using bool
Game::savestatsandsettings()
* The one caller of bool Game::savestats(bool) is now using bool
Game::savestats()
While there already exists an option to skip the fake loading screen
entirely (without requiring an ACTION press), there are several reasons
for including this option as well:
* So people upgrading from 2.2 won't have to sit through the fake
loading screen the first time they open 2.3.
* So if people are too lazy to use the existing option, they can use
this one instead.
* So tool-assisted speedruns (TASes) of this game can skip the fake
loading screen without requiring an existing settings.vvv beforehand.
This last one is the biggest reason for me, since I'm not sure what
TASVideos.org rules are regarding existing save files, but with this
change nobody has to worry about their rules and can safely just
press ACTION to skip the fake loading screen automatically.
`success = success && savesettings();` is now changed to
`success &= savesettings();`. It's bitwise, and I think C++ should have
had a &&= for completeness, but it shouldn't matter here.
Changing settings would most of the time attempt to save unlock.vvv and
now also settings.vvv, but there would be no feedback whether the files
have been saved successfully or not. Now, if saving fails when changing
settings in the menu, a warning message will be shown. The setting will
still be applied of course, but the user will be informed it couldn't
be saved. This message can be silenced until the game is restarted,
because I can imagine this could get very annoying when someone already
knows their settings aren't writeable.
Also, some options didn't even save settings in the first place. These
are turning off invincibility, and by coincidence precisely all the
options in the advanced options menu. I made sure these options now do
so.
As part of fixing #464, I'll need to move these pieces of code around
easily. In #220 I just kind of shoved them awkwardly in whatever
fixed function would be last called in the gamestate loop, which I
shouldn't have done as I've now had to make formal fixed-render
functions anyway. Because these fixed functions need to be called
directly before a render function, and I'm fixing the order to put
render functions in their proper place, so I need to be able to move
these around easily, and making them function calls instead of inlined
makes them easier to manipulate.
Today, I saw a video posted by Chelito on the VVVVVV speedrunning
Discord where he died inside a gravitron square over and over after the
Gravitron in Intermission 2 ended.
https://cdn.discordapp.com/attachments/234787368522088448/779074864660480020/What.mp4
This is caused by the fact that after the Gravitron ends, the game no
longer considers you to be inside swnmode, so it won't move the enemies
offscreen when you die.
To fix this, after the Gravitron ends, the game will switch to swngame
8, where it will keep you inside swnmode until all gravitron squares are
offscreen. This means that gravitron squares that are onscreen after the
Gravitron ends will be moved offscreen if you die, preventing the above
death loop from happening.
PR #468 made it so you can use the menus while in a cutscene, in order
to counteract softlocks. However, this has resulted in more
unintentional behavior:
- `gamemode(teleporter)` breaks when opening the ENTER menu (Misa
mentioned this)
- The player can now interrupt shakes and walks, and have their timers
run out before resuming the cutscene
- After completing the game, the player can warp to the ship while a
dialogue is active, and prevent themselves from advancing text (plus
it's always rude to just teleport away while someone's talking)
- The player can peek at the map before hidecoordinates is run, and can
also peek at what the game does with missing/rescued crewmates during
cutscenes
This commit fixes the latter two issues. While a script is running,
only the SAVE tab is now available. Therefore the player can still get
themselves out of softlocks as intended, but they do things like
looking at the map or teleporting away during a cutscene.
It wasn't a direct duplicate of key.sensitivity, but it was still
basically the same thing. Although to be fair, at least the case-switch
conversion didn't get duplicated everywhere unlike game.slowdown.
So now key.sensitivity functions the same as game.controllerSensitivity,
and it only gets converted to its actual value whenever a joystick input
happens in key.Poll(), unlike previously where it got converted every
single frame on the title screen (there was even a comment that said
"TODO bit wasteful doing this every poll").
game.gameframerate seems to exist for converting the value of
game.slowdown into an actual timestep value, when really the timestep
value should just use game.slowdown directly with a fast lookup table.
Otherwise, there's a bunch of duplicated game.slowdown case-switches
everywhere, which adds up to a large, annoying pile should the values be
changed in the future. But now the duplicate variable has been removed,
and with it, all the copy-pasted case-switches.
Also, the game speed text rendering in Menu::accessibility and
Menu::setslowdown has been factored out to a function and de-duplicated
as well.
There were some duplicate Screen configuration variables that were on
Game, when there didn't need to be.
- game.fullScreenEffect_badSignal is a duplicate of
graphics.screenbuffer->badSignalEffect
- game.fullscreen is a duplicate of !graphics.screenbuffer->isWindowed
- game.stretchMode is a duplicate of graphics.screenbuffer->stretchMode
- game.useLinearFilter is a duplicate of
graphics.screenbuffer->isFiltered
These duplicate variables have been removed now.
I put indentation when handling the ScreenSettings struct in main() so
the local doesn't live for the entirety of main() (which is the entirety
of the program).
Apparently, the amount of digits in a commit hash that git will output
varies depending on how many objects are in the repository that the hash
gets pulled from. The more objects, the more digits needed to avoid a
hash collision.
Sources:
https://stackoverflow.com/q/18134627/#comment26560283_18134919https://stackoverflow.com/a/21015031/
So that means we'll have to dynamically account for the length of the
commit hash in order to get it properly right-aligned with the rest of
the text.
There are probably going to be situations where we'll want to compile in
release mode, but still want the hash and don't want Steam/GOG enabled.
So Ethan can use OFFICIAL_BUILD when tagging major versions of the game.
For some reason, music.usingmmmmmm automatically gets set to true in
musicclass::init(). I assume this was because it would get re-assigned
by game.usingmmmmmm in the game startup code anyway, but now that
musicclass::init() can be called more than once, this variable will just
get set to true when it shouldn't be, causing a confusing desync just
like the one I described in my previous commit, where you would have
PPPPPP or MMMMMM on the title screen, but closing the game and
re-launching it would play the other soundtrack instead.
Again, these duplicate variables should be removed, but that's going to
be a separate patch. In the meantime, I'm removing this variable
assignment.
musicclass::init() re-initializes every attribute of musicclass
unnecessarily, when initialization should be put in a constructor
instead. This is bad, because music.init() gets called whenever we enter
and exit a custom level that has assets.
Otherwise, this would result in a bug where music.usingmmmmmm would be
reset, causing you to revert to PPPPPP on the title screen whenever you
enter a level with MMMMMM selected and exit it.
This also causes a confusing desync between game.usingmmmmmm and
music.usingmmmmmm since the values of the two variables are now
different (these duplicate variables should probably be removed, too,
and a lot of other duplicate variables like these exist, too, which are
a real headache). Which means despite MMMMMM playing on the title
screen, exiting the game and re-launching it will play PPPPPP instead.
What's even more is that going to game options and switching to PPPPPP
will play PPPPPP, but afterwards launching and re-entering will play
MMMMMM. Again, having duplicate variables is very bad, and should
probably be fixed, but that's a separate patch.
...you die and the platform's x-coordinate is to the left of x=152.
Which means if you die and the platform isn't completely clear of the
space of its adjacent disappearing platform.
The block needs to be updated accordingly with calls to
obj.nocollisionat() and obj.moveblockto(), else the block will simply be
left behind and the platform will no longer have any collision. This is
in contrast to 2.2 behavior, where the platform would simply
unconditionally create a new block, which would actually end up with a
duplicate block since the previous block didn't get cleaned up, but this
didn't cause any problems because the room was carefully designed so you
would never be able to touch that previous block after you died and
respawned at the checkpoint. But it's still there.
I also added comments to document what this kludge code did, because
otherwise it would be mysterious to readers who are unfamiliar with it.
Fixes#543.
What's the difference between a slash sign and a percent sign? Well, a
percent sign is just a slash sign with two extra oranges in between, but
those two oranges make a huuuuge difference...
I can't really make the filter update only every timestep, because it's
per-pixel and operates on deltaframes too, so it TECHNICALLY runs faster
in over-30-FPS mode than not. That said, it's not really noticeable, the
filter doesn't look bad for updating more often or anything. However, I
can at least interpolate the scrolling, so it's smooth in over-30-FPS
mode.
Originally this function was made because it needed to be exported to
gameinput(), but this piece of code is actually also used in
gamecompletelogic(). So it's good to de-duplicate it here, too.
Assigning these variables is now wholly unnecessary ever since #522 got
merged, and in fact setting graphics.backgrounddrawn to false actually
causes the warp background to "skip" when the map screen gets closed. So
this fixes that bug, too.
This kludge variable was used to re-set the warp background after coming
back from the in-game settings menus. But since #522 got merged, this
has no longer been necessary.
This check is clearly meant for destroying the factory clouds in the
room "Level Complete!" in the main game, but it covers all rooms on row
8, instead of only (13,8). Adding an x-room check restricts this
behavior to only (13,8).
Trinket9 reported that this weird behavior of destroying specifically
above y-position 60 was undesirable, since they were creating an enemy
with this `behave` in a room on row 8 and it kept disappearing
instantly.
First, the variable has been inverted, because it's bad practice to have
booleans with negative names. Secondly, instead of magically setting a
signaling variable when calling fadeout(), fadeout() now has a parameter
to set the quick_fade attribute, which is much cleaner than doing the
magical assignment that could potentially look unrelated.
fadeoutqueuesong basically does the same thing as nicechange - they both
queue a song to be played when the current track is done fading out.
Except, for some reason, I decided to add fadeoutqueuesong instead of
using the existing nicechange/nicefade system.
This has consequences where fadeoutqueuesong would step on the toes of
nicechange. In the case of #390, nicechange would say "let's play
Potential for Anything" when entering the Super Gravitron, but
fadeoutqueuesong had previously said "let's play Pipe Dream" because of
the player having just exited the Super Gravitron. fadeoutqueuesong took
priority because it came first in musicclass::processfade(), and when it
called play(), the Mix_PlayingMusic() in the nicechange check afterwards
would say music would already be playing at that point, so the
nicechange wouldn't take effect.
In the end, the solution is to just merge the new system into the
already-existing system.
Fixes#390.
Just looked over this and had to do a double-take. It should be
num_mmmmmm_tracks, not num_pppppp_tracks, because MMMMMM comes first in
the vector of music tracks.
Here's what causes #401: After the fade to menu delay ticks down to 0,
the game calls game.quittomenu(), but the rest of mapinput() still
executes. This means that the block that detects your ACTION press gets
executed, because there's a check that fadetomenudelay is less than or
equal to 0, and, well, it is.
So if you've pressed ACTION on the exact frame that it counts down to 0,
then the game detects your ACTION press, then processes it accordingly,
and then sets the fadetomenudelay, which means it'll get reactivated the
next time you open the map screen. But at this point, you get sent to
TITLEMODE, because game.quittomenu() set game.gamestate accordingly.
(This is why resetting game.fadetomenu or game.fadetomenudelay in
game.quittomenu() or script.hardreset() won't fix this bug.)
The solution here is to add a game.fadetomenu check to the ACTION press
processing.
Same-frame state transition logic is hard... actually, any sort of thing
where two things happen on the same frame is really annoying.
This also applies to fadetolab and fadetolabdelay, too.
Fixes#401.
The game previously did this dumb thing where it lumped in all its
settings with its file that tracked your records and unlocks,
`unlock.vvv`. It wasn't really an issue, until 2.3 came along and added
a few settings, suddenly making a problem where 2.3 settings would be
reset by chance if you decided to touch 2.2.
The solution to this is to move all settings to a new file,
`settings.vvv`. However, for compatibility with 2.2, settings will still
be written to `unlock.vvv`.
The game will prioritize reading from `settings.vvv` instead of
`unlock.vvv`, so if there's a setting that's missing from `unlock.vvv`,
no worries there. But if `settings.vvv` is missing, then it'll read
settings from `unlock.vvv`. As well, if `unlock.vvv` is missing, then
`settings.vvv` will be read from instead (I explicitly tested for this,
and found that I had to write special code to handle this case,
otherwise the game would overwrite the existing `settings.vvv` before
reading from it; kids, make sure to always test your code!).
Closes#373 fully.
This fixes a deltaframe glitch where the "- Press ENTER to Teleport -"
prompt would show up for a split second if you exited the game while the
prompt was fully faded in, and then re-entered it.
cppcheck said: "Logical disjunction always evaluates to true".
Yes. Yes it does. Whoops. I learned how to specify ranges like this in
high school math and still screw it up...
In C++, when you have two variables in different scopes with the same
name, the inner scope wins. Except you have to be really careful because
sometimes they're not (#507). So it's better to just always have unique
variable names and make sure to never clash a name with a variable in an
outer scope - after all, the C++ compiler and standard might be fine
with it, but that doesn't mean humans can't make mistakes reading or
writing it.
Usually I just renamed the inner variables, but for tx/ty in editor.cpp,
I just got rid of the ridiculous overcomplicated modulo calculations and
replaced them with actual simple modulo calculations, because the
existing ones were just ridiculous. Actually, somebody ought to find
every instance of the overcomplicated modulos and replace them with the
actual good ones, because it's really stupid, quite frankly...
Whenever you delete all your save data, your settings aren't changed at
all, and you could resave them if you fiddled with a setting somewhere.
But the full game doesn't count Flip Mode as a setting, instead it
counts it as an unlock. This means deleting your save data would unset
Flip Mode in M&P, which would seem weird because in M&P it's just a
simple setting.
For consistency, Flip Mode shouldn't be unset when deleting save data in
M&P.
This commit fixes a bug that also sometimes occurred in 2.2, where the
teleporter sprite would randomly turn into a solid color and just be a
solid circle with no detail.
Why did this happen? The short answer is an incorrect lower bound when
clamping the teleporter sprite index in `Graphics::drawtele()`. The long
answer is bad RNG with the teleporter animation code. This commit fixes
the short answer, because I do not want to mess with the long answer.
So, here is what would happen: the teleporter's `tile` would be 6. The
teleporter code decrements its `framedelay` each frame. Then when it
reached a `framedelay` of 0, it would call `fRandom()` and essentially
ask for a random number between 0 and 6. If the RNG ended up being
greater than or equal to 4, then it would set its `walkingframe` to -5.
At the end of the routine, the teleporter's `drawframe` ends up being
its `tile` plus its `walkingframe`. So having a `walkingframe` of -5
here is fine, because its `tile` is 6.
Until it isn't. When its `tile` becomes 2, it still keeps its
`walkingframe` around. The code that runs when its `tile` is 2 does have
the possibility of completely resetting its `walkingframe` to be in
bounds (in bounds after its `tile` is added), but that only runs when
its `framedelay` is 0, and in the meantime it'll just use the previous
`walkingframe`.
So you could have a `walkingframe` of -5, plus a `tile` of 2, which
produces a `drawframe` of -3. Then `Graphics::drawtele()` will clamp
that to 0, which just means it'll draw the teleporter backing, and the
teleporter backing is a simple solid color, so the teleporter will end
up being completely and fully solid.
To fix this, I just made `Graphics::drawtele()` clamp to 1 on the lower
bound, instead of 0. So if it ever gets passed a negative teleporter
index, it'll just draw the normal teleporter sprite instead, which is
better.
This fixes the draw order by drawing all other entities first, before
then drawing all humanoids[1] after, including the player afterwards.
This is actually a regression fix from #191. When I was testing this, I
was thinking about where get a crewmate in front of another entity in
the main game, other than the checkpoints in Intermission 1. And then I
thought about the teleporters, because I remember the pre-Deep Space
cutscene in Dimension Open looking funny because Vita ended up being
behind the teleporter. (Actually, a lot of the cutscenes of Dimension
Open look funny because of crewmates standing behind terminals.)
So then I tried to get crewmates in front of teleporters. It actually
turns out that you can't do it for most of them... except for Verdigris.
And then that's what I realized why there was an oddity in WarpClass.cpp
when I was removing the `active` system from the game - for some reason,
the game put a hole in `obj.entities` between the teleporter and the
player when loading the room Murdering Twinmaker. In a violation of
Chesterton's Fence (the principle that you should understand something
before removing it), I shrugged it off and decided "there's no way to
support having holes with my new system, and having holes is probably
bad anyway, so I'm going to remove this and move on". The fact that
there wasn't any comments clarifying the mysterious code didn't help
(but, this *was* 2.2 code after all; have you *seen* 2.2 code?!).
And it turns out that this maneuver was done so Verdigris would fill
that hole when he got created, and Verdigris being first before the
teleporter would mean he would be drawn in front of the teleporter,
instead of being behind it. So ever since
b1b1474b7b got merged, there has actually
been a regression from 2.2 where Verdigris got drawn behind the
teleporter in Murdering Twinmaker, instead of properly being in front of
it like in 2.2 and previous.
This patch fixes that regression, but it actually properly fixes it
instead of hacking around with the `active` system.
Closes#426.
[1]: I'm going to go on a rant here, so hear me out. It's not explicitly
stated that the characters in VVVVVV are human. So, given this
information, what do we call them? Well, the VVVVVV community (at least
the custom levels one, I don't think the speedrunning community does
this or is preoccupied with lore in the first place) decided to call
them "villis", because of the roomname "The Villi People" - which is
only one blunder in a series of awful headcanons based off of the
assumption that the intent of Bennett Foddy (who named the roomnames)
was to decree some sort of lore to the game. Another one being
"Verdigris can't flip" because of "Green Dudes Can't Flip". Then an OC
(original character) got named based off of "The Voon Show" too. And so
on and so forth.
"Humanoid" is just a word for "crewmate or player" but without having to
say "crewmate or player". This is just to make it so humanoids get drawn
after all other entities get drawn, meaning humanoids will be drawn on
top.
I'm going to refactor drawing an entity out to a separate function, and
since I'm going to do that, I might as well make some things const to
clarify intent first and foremost and possibly improve performance or
compiler optimization.
My previous custom level forwards compatibility would only work if you
saved to the same filename as you read from. But what if you saved to a
new filename? Well, your extra XML is lost.
Not to worry, I've introduced a variable that remembers the filepath of
the currently-loaded level (the existing `filename` attribute is kind of
weird and not really suited for this purpose, so). I tried to make it a
simple `const char*`, but it turns out that's bad when you take the
`c_str()` of an `std::string` that then gets destroyed, so it's best to
use `std::string` in an `std::string` world.
So now when you save a level, it'll attempt to open the original file,
and if that doesn't exist then your extra XML gets lost anyway, but I
shouldn't be expected to keep your XML if you delete the original file.
Custom level files now have forwards compatibility - except that some
XML objects will simply discard contents and attributes they don't see
fit. For instance, edentities and level properties will not preserve
newer attributes or contents.
This is because preserving them would require having to track the XML
object as part of the edentity internally, because the edentity might
change places or even be deleted throughout the course of editing
someone's level.
I opted to not add support for preserving objects like these, because
frankly, the put-everything-in-one-file level format was and still is a
terrible idea, and we should probably switch to a new format in the
future that isn't single-file. So when that happens, there won't be a
need to preserve XML attributes on edentities.
When I encountered this function, I asked myself, why make a dedicated
function instead of casting to int?
Well, as it turns out, you can't just cast to int, because you have to
convert it to a string by doing help.String(number).c_str(), like all
the other ints. So it's actually more wasteful to do it the normal way
instead of using BoolToString().
That is, until my recent changes came along and made it so that you can
just provide an int and it'll automatically be converted to a string, no
questions asked. Now, it's more optimal to do a straight cast to int
instead of having to go through a middleman function. So this function
is getting axed in favor of casting to int instead.
This file is probably the biggest one, as there will be more settings
added in the future and we don't want people's settings to be erased. Of
course, this file will be migrated to a settings.vvv sometime later in
2.3 in order to prevent 2.2 from erasing 2.3 settings.
These XML functions will be useful for de-duplicating copy-pasted XML
handling code, while also making it so elements will get updated in
place instead of being thrown out and starting from scratch every time a
file is saved.
Now that tower, title, and horizontal/veritcal warp backgrounds all use
separate buffers, there's no longer any need to temporarily store
variables as a workaround for the buffers stepping on each other.
Instead of using the same tower buffer that gets used for towers, use a
separate buffer instead so there's no risk of stepping on the tower
buffer's toes at the wrong point in time.
This commit combined with the previous one fixes#369.
With the previous commit in place, we can now simply move some usages of
the previous towerbg to use a separate object instead. That way, we
don't have to mess with a monolithic state, or some better way to phrase
what I just said, and we instead have two separate objects that can
coexist side-by-side.
Previously, the tower background was controlled by a disparate set of
attributes on Graphics and mapclass, and wasn't really encapsulated. (If
that's what that word means, I don't particularly care about
object-oriented lingo.) But now, all relevant things that a tower
background has has been put into a TowerBG struct, so it will be easy to
make multiple copies without having to duplicate the code that handles
it.
Yet another set of temporary variables is on a global class when they
shouldn't be. These two are only used in tower background functions and
are never used anywhere else, so they're clearly temporaries.
For some reason, this `tl` is a `point`? But the only other time the
name `tl` is used elsewhere in the code is a float on a `textboxclass`.
Regardless, this is unused.
This function was only used in assigments to mapclass::towercol. But
that variable is unused, and has been removed, so after removing that
variable, this one is unused, too.
On the deltaframes of the tower background, there would be "pixel bleed"
if the tower background would be scrolling from the top. This is because
there wouldn't be any more pixels from above the screen to grab, because
the background rendering functions didn't draw any pixels above the
screen. But they couldn't draw any pixels above the screen, because that
was simply the end of the buffer. But now that the buffer is expanded,
we can now draw above the screen, and fix this glitchy interpolation
rendering.
In order to fix the weird title screen pixels at the top on deltaframes,
we'll need to have a bit more space at the top. Also to the left, in
case we need a background to scroll from the left in the future.
These commands now use the INBOUNDS_ARR() macro to convey intent, and to
make sure that if the size of the array changes in the future, that the
bounds check wouldn't end up being wrong. Also fixed some code style for
the flag() and ifcrewlost() commands.
Scripting crewmates apparently have a specific hardcoded rule in their
follow-player code that makes it so if they're in (10,5) and are to the
left of the line x=155, they will refuse to continue following the
player. This was clearly done to make it so Vitellary in the main game
wouldn't overlap the teleporter, and that was clearly done to make it so
it wouldn't look like he would go behind the teleporter, which would
look weird (I also noticed this in #513).
I stumbled across this code and thought that just like other weird
main-game code that shouldn't apply in custom levels (#136, #137, #144),
this should be fixed, too.
The window icon is simply another asset that can be customized by level
makers. And in fact, one of my levels changes the window icon. It's
simply named VVVVVV.png, but it doesn't sit in the graphics folder,
rather it sits in the root VVVVVV directory.
I noticed that this asset was missed when per-level assets loading was
added, so I decided to add it in.
There's a NULL check on screenbuffer because reloadresources() gets
called before screenbuffer's init() does.
The intent of #504 was to make it so oldxp/oldyp would never be messed
with for over-30-FPS stuff, but I forgot that I changed these
assignments in my over-30-FPS patch when I was doing #504. So these
assignments have been restored to the way they were in 2.2, and is
fixed now.
While my previous commit fixes the glitchy y-position when you get stuck
inside a conveyor, I noticed that getting inside a conveyor seems to
always push the player out, despite conveyors sharing the same code with
moving platforms, which has code to temporarily disable their own
collision when the player gets stuck inside them, so that the player
DOESN'T get pushed out.
Well, it turns out that the reason this happens is because conveyors in
a room that get placed during mapclass::loadlevel() get tile 1 placed
underneath them. This is mostly so moving platforms will collide with
them, because otherwise platforms don't collide with other platforms,
and a conveyor is considered a platform.
This means that a conveyor without any tiles behind it will simply get
the player stuck if they get inside it, and the player won't be pushed
out. This is bad, because conveyors don't move, so they'll be stuck
there forever until they press R (or save, quit, and load). This
situation doesn't come up in the main game, but it COULD come up in
custom levels that use the internal createentity() command to create
conveyors that don't have any tiles behind them.
It seems good to fix this as well, while we're at it fixing conveyor
physics, so I'm fixing this as well.
There is this issue with conveyors where if you collide with them, your
intended next y-position doesn't get updated to the position of the
conveyor, and then your y-position gets set to your intended next
y-position. This also applies to horizontally moving platforms.
This bug used to not produce any problems, if at all, until #502 got
merged. Since then, respawning from checkpoints that are on conveyors
would sometimes not update your y-position at all, making it possible to
get stuck inside a death loop that would require you to exit the game
and re-enter it.
But you can always reliably create this bug simply by going into the
editor and placing down a conveyor and checkpoint on top of each other.
Then enter and exit playtesting a bunch of times, and you'll notice the
glitchy y-position Viridian keeps taking on.
The root cause of this is how the game moves the player whenever they
stand on the top or bottom of a conveyor (or a horizontally moving
platform). The game sets their intended next x-position (newxp), then
calls obj.entitymapcollision() on them. This would be okay, except their
intended next y-position (newyp) doesn't get set along with their newxp,
so entitymapcollision() will use the wrong newyp, then find that there
is nothing that will collide with the player at that given newyp, then
update the yp of the player to the wrong newyp.
So, the platform logic simply doesn't set the player's newyp. Why does
the player have the wrong newyp? It's because moving platforms (and
conveyors) are updated first before all other entities are, and this
includes the code that checks the player for collisions with moving
platforms. That's right: the moving platform collision code gets ran
before the player properly gets updated. This means that the game will
use the newyp of the previous frame, which could be anything, really,
but it most likely just means the intended next y-position of the player
gets canceled, leaving the player with the same y-position they had
before.
Okay, but this bug only seems to happen when you put a checkpoint inside
a conveyor (or a moving platform that hasn't started moving yet) and
start playtesting from it, so why doesn't this bug happen more often,
then? Well, it's probably because of luck and coincidence. Most of the
time, if you're colliding with a conveyor or horizontally moving
platform, you probably have a correct newyp from the previous frame of
the game, so there'd be no problems. And before #502 got merged, this
previous frame would be provided by the player having to fall to the
surface due to the y-offset of their savepoint. However, if you make it
so that you immediately teleport on to a conveyor (because you died),
then this bug will rear its ugly head.
I got this warning during compilation because there were two nested for
loops both defining i. Better to have different names to make sure some
compilers won't overwrite the outer variable with the inner one.
I was investigating a desync in my Nova TAS, and it turns out that
the gravity line collision functions check for the `oldxp` and `oldyp`
of the player, i.e. their position on the previous frame, along with
their position on the current frame. So, if the player either collided
with the gravity line last frame or this frame, then the player collided
with the gravity line this frame.
Except, that's not actually true. It turns out that `oldxp` and `oldyp`
don't necessarily always correspond to the `xp` and `yp` of the player
on the previous frame. It turns out that your `oldyp` will be updated if
you stand on a vertically moving platform, before the gravity line
collision function gets ran. So, if you were colliding with a gravity
line on the previous frame, but you got moved out of there by a
vertically moving platform, then you just don't collide with the gravity
line at all.
However, this behavior changed in 2.3 after my over-30-FPS patch got
merged (#220). That patch took advantage of the existing `oldxp` and
`oldyp` entity attributes, and uses them to interpolate their positions
during rendering to make everything look real smooth.
Previously, `oldxp` and `oldyp` would both be updated in
`entityclass::updateentitylogic()`. However, I moved it in that patch to
update right before `gameinput()` in `main.cpp`.
As a result, `oldyp` no longer gets updated whenever the player stands
on a vertically moving platform. This ends up desyncing my TAS.
As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the
function responsible for moving the player whenever they stand on a
vertically moving platform) makes it so that my TAS syncs, but the
visuals are glitchy when standing on a vertically moving platform. And
as much as I'd like to get rid of gravity lines checking for whether
you've collided with them on the previous frame, doing that desyncs my
TAS, too.
In the end, it seems like I should just leave `oldxp` and `oldyp` alone,
and switch to using dedicated variables that are never used in the
physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and
replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS
patch added, with `lerpoldxp` and `lerpoldyp` instead.
After doing this, and applying #503 as well, my Nova TAS syncs after
some minor but acceptable fixes with Viridian's walkingframe.
This commit restores the evaluation order of moving platforms and conveyors to
be what it was in 2.2. The evaluation order changed in 2.3 after the patchset
to improve the handling of the `obj.entities` and `obj.blocks` vectors (#191).
By evaluation order, I'm talking about the order in which platforms and
conveyors will be evaluated (and thus will take priority) if Viridian stands
on both a conveyor or platform at once, and they either have different speeds
or are pointing in different directions. Nowhere in the main game is there a
place where you can stand on two different conveyors/platforms at once, so
this is solely within the territory of custom levels, which is my specialty.
So what caused this evaluation order to change? Well, every moving platform
and conveyor in the game is actually made up of two objects: an entity, and a
block. The entity is the part that moves around, and the block is the part
that actually has the collision.
But if the entity is the part that moves around, and entities and blocks are
in entirely separate vectors, how is the block part going to move along with
it? Well, maybe you'd guess some sort of unique ID system, but spend some time
digging around the code and you won't find any trace of any (there's no
attribute on an entity to store such an ID, for starters).
Instead, what the game does is actually remove all blocks that coincide with
the exact top-left corner of the entity, and then create a new one. Destroying
and creating blocks like this all the time is hugely wasteful, but hey, it
worked.
So why did the evaluation order change in 2.3? Well, to understand that,
you'll need to understand 2.2's `active` system. Instead of having an object
be real simply by virtue of it existing, 2.2 had this system where the object
was only real if it had its `active` attribute set to true. In other words,
you would be looking at a fake object that didn't actually exist if its
`active` attribute was false.
On the surface, this doesn't seem that bad. But this can lead to "holes" in a
given vector of objects. A hole is simply an inactive object neighbored by
active objects (or the inactive object could be the first one in the vector,
but then have an active object immediately following it).
If you have a vector of 3 objects, all of them active, then removing the
second one will result in the vector containing an active object, followed by
an inactive object, followed by an active one. However, since the switch to
more properly use vectors instead of relying on this `active` system, there's
no longer any way for holes to exist in a vector. Properly removing an object
from a vector will just shift the rest of the objects down, so if we remove
the second object after the vector fix, then this will simply move the third
object into the slot of where the second object used to be.
So, what happens if you destroy a block and then create a new one in the
`active` system? Let's say that your `obj.blocks` looks like this, and here
I'm denoting each block by writing out its coordinates:
[30,60] [70,90] [80,100]
and that you want to update the position of the second one, because the entity
that that blocks belongs to has been updated. Okay, so, you delete that block,
which then makes things look like this:
[30,60] [-] [80,100]
and then afterwards, you create a new block with the updated position,
resulting in this:
[30,60] [74,90] [80,100]
Since `entityclass::createblock()` will find the first block slot that has a
false `active` attribute, it puts the new object in the same slot as the old
one. What has been essentially done here is that the slot of the block has
basically been reserved for the new block with the new position. Here, the
evaluation order of each block will stay the same.
But then 2.3 comes along and changes things up. So we start with an
`obj.blocks` like this again:
[30,60] [70,90] [80,100]
and we want to update the second block, like before. So we remove the second
block, resulting in this:
[30,60] [80,100]
It should be obvious that unlike before, where the third block stayed in the
third slot, the third block has now been moved to the second slot. But
continuing on; we are now going to create the new block with its updated
position, resulting in this:
[30,60] [80,100] [70,90]
At this point, we can see that the evaluation order of these blocks has been
changed due to the fact that the third block has now been moved to the slot
that was previously the slot of the second block.
So what can we do about this? Well, we can basically emulate what VVVVVV did
in 2.2, which is disable a block without actually removing it - except I'm not
going to reintroduce an `active` attribute or anything. I'll disable the
collision of all blocks at a certain position by setting their widths and
heights to 0, and then re-enable them later by finding the first block at that
same position, updating its position, and re-assigning its width and height
again.
The former is what `entityclass::nocollisionat()` does; the latter is what
`entityclass::moveblockto()` does. The former mimicks turning off the `active`
attribute of all blocks sharing a certain top-left corner; the latter mimicks
creating a new block - and it will only do this for one block, because
`entityclass::createblock()` in 2.2 only looked for the first block with a
false `active` attribute.
Now, some quirks relied on the previous behavior of destroying and creating
blocks, but all of these quirks have been preserved with the way I implemented
this fix.
The first quirk is that platforms passing through 0,0 will destroy all spike
hitboxes, script boxes, activity zones, and one-way hitboxes in the room. The
hitboxes of moving platforms, disappearing platforms, 1x1 quicksand, and
conveyors will not be affected.
This is a consequence of the fact that the former group uses the `x` and `y`
of their `rect`, while the latter group uses the `xp` and `yp` attributes. So
the `xp` and `yp` of the former are both 0. Meaning, a platform passing
through 0,0 destroys them all.
Having these separate coordinates seems like an artifact from the Flash days.
(And furthermore, there's an unused `x` and `y` attribute on all blocks,
making for technically three separate sets of coordinates! This should
probably be cleaned up, except for what I'm about to say...) But actually, if
you merge both sets of coordinates into one, this lets moving platforms
destroy script boxes and activity zones if it passes through the top-left
corner of them, which is probably far worse than the destruction being
localized to a specific coordinate that would never likely be reached
normally.
This quirk is preserved just fine without any special-casing, because instead
of destroying all blocks at 0,0, they just get disabled, which does the same
job. This quirk seems trivial to fix if I made it so that the position of a
platform's block was updated instantaneously instead of having one step to
disable it and another step to re-enable it, but I aim to preserve as much
quirks as possible.
The second quirk is that a moving platform passing through the top-left corner
of a disappearing platform or 1x1 quicksand will destroy the block of that
disappearing platform. This is because, again, when a moving platform updates,
it destroys all blocks at its previous position, not just only one block. This
is automatically preserved because this commit just disables the block of the
disappearing platform instead of removing it. Just like the last one, this
quirk seems extremely trivial to fix, and this time by simply making it so
`entityclass::nocollisionat()` would have a `break` statement, i.e. only
disabling the first block it finds instead of all blocks it finds, but I want
to keep all quirks that are possible to keep.
The last quirk is that, apparently, in order to prevent pushing the player
vertically out of a moving platform if they get inside of one, the game
destroys the block of the moving platform. If I had missed this edge case,
then the block would've been destroyed, leaving the moving platform with no
collision. But I caught it in my testing, so the block gets disabled instead
of destroyed. Also, it seems obtuse for those who don't understand why a
platform's block gets destroyed or disabled whenever the player collides with
it in `entityclass::collisioncheck()`, so I've put up a comment documenting it
as well.
The different platform evaluation order desyncs my Nova TAS, but after
applying this patchset and #504, my TAS syncs fine (save for the different
walkingframe from starting immediately on the ground instead of in the air
(#502), but that's minor and can be easily fixed).
I've attached a test level to the pull request for this commit (#503) to
demonstrate that this patchset not only fixes platform evaluation order, but
preserves some bugs and quirks with the existing block system.
The first room demonstrates the fixed platform evaluation order, by stepping
on the conveyors that both point into each other. In 2.2, Viridian will move
to the right of the background pillar, but in 2.3, Viridian will move to the
left of the pillar. However, after applying this patch, Viridian will now move
to the right of the pillar once again.
The second room demonstrates that the platform-passing-through-0,0 trick still
works (as explained above).
The last room demonstrates that platforms passing through the top-left corners
of disappearing platforms or 1x1 quicksand will remove the blocks of those
entities, causing Viridian to immediately pass through them. This trick is
still preserved after my patchset is applied.
Previously, setting the actual volume of the music was all over the
place. Which isn't bad, but when I added being able to press N to mute
the music specifically, I should've made it so that there would be a
volume variable somewhere that the game looks at if the music is
unmuted, and otherwise sets the actual volume to 0 if the game is muted.
This resulted in things like #400 and #505 and having to add a bunch of
special-cased checks like game.musicmuted and game.completestop. But
instead of adding a bunch of special-case code, just make it so there's
a central place where Mix_VolumeMusic() actually gets called, and if
some piece of code wants to set the music volume, they can set
music.musicVolume. But the music handling logic in main.cpp gets the
final say on whether to listen to music.musicVolume, or to mute the game
entirely.
This is how the music handling code should have been from the start
(when pressing N to mute the music was added).
Fixes#505.
The value of the macro might not change in the future, but it's there
for a reason. That reason being to improve code readability, because
otherwise 128 would just be a magic number that plopped in out of
nowhere. Sometimes the game uses MIX_MAX_VOLUME, other times it uses
128, so to be consistent I'm just going to enforce MIX_MAX_VOLUME
entirely.
This reverts commit cf5ad166e3.
My implementation will make it so single-case patches like this commit
won't be necessary anymore (there's no need to add a special-case check
for game.musicmuted, the way that I'm gonna do it). In fact, it's better
if I just revert the commit entirely.
It seems like the start point of a custom level and all checkpoints in
the game end up putting your spawn point one pixel away from the surface
it touches, which seems like an oversight. I'm going to enforce some
consistency here and make it so that all spawn points, whenever you
start from a start point or a checkpoint, will always be correctly
positioned flush with the surface they're standing on, and not one pixel
more or less than that.
An exotic checkpoint is a checkpoint with a sprite that is neither the
flipped checkpoint nor unflipped checkpoint. More specifically, it's a
checkpoint whose edentity p1 attribute is something other than 0 or 1.
Normally, whenever you touch an exotic checkpoint in-game, your
savepoint's y-position and gravitycontrol don't get touched. However, in
the editor, spawning from an exotic checkpoint means that your
gravitycontrol gets set to a value that is neither 0 nor 1. In this
invalid gravitycontrol state, Viridian is treated like they're flipped,
but they cannot unflip.
This is an inconsistency between the editor and in-game, so I'm fixing
it. Now, spawning from an exotic checkpoint in the editor will just set
your gravitycontrol to 0, i.e. unflipped.
So, 77a636509b fixed the fact that you
only got 1 frame of onground/onroof when standing on a vertical moving
platform, but removing those lines entirely means that it takes 1 frame
before the onground/onroof of 2 actually takes effect. This desyncs my
Nova TAS, so it seems important to fix.
The onroof/onground attributes are used to determine if the player is
standing on a surface and is eligible to flip. Most notably, it is an
integer and not a boolean, and it starts at 2, giving the player 2
frames to edge-flip, i.e. they can still flip 2 frames after walking off
an edge.
However, these attributes are unnecessarily reassigned in
movingplatformfix() (which is the function that deals exclusively with
vertically-moving platforms; horizontal moving platforms get their own
hormovingplatformfix()). Whoever wrote this misunderstood what
onroof/onground meant; they thought that they were booleans, and so set
them to true, instead of the proper value of 2. This ends up setting
onroof/onground to 1 instead of 2, causing a discrepancy with vertical
moving platforms and the rest of the surfaces in the game.
The bigger mistake here is duplicating code that never needed to be
duplicated. The onroof/onground assignment in gamelogic() works
perfectly fine for vertical moving platforms. Indeed, after testing it
with libTAS, I can confirm that removing the duplicate assignments
restores being able to edge-flip off of moving platforms with 2 frames
of leeway, instead of only 1 frame. It also doesn't change how long it
takes for the onroof/onground to get set when the player is recognized
as standing on a vertically-moving platform, either.
And so, it's better to not duplicate this code, because when you
duplicate it you run the risk of making a mistake, as I just
demonstrated.
By "unnecessary qualifiers to self", I mean something like using the
'game.' qualifier for a variable on the Game class when you're inside a
function on the Game class itself. This patch is to enforce consistency
as most of the code doesn't have these unnecessary qualifiers.
To prevent further unnecessary qualifiers to self, I made it so the
extern in each header file can be omitted by using a define. That way,
if someone writes something referring to 'game.' on a Game function,
there will be a compile error.
However, if you really need to have a reference to the global name, and
you're within the same .cpp file as the implementation of that object,
you can just do the extern at the function-level. A good example of this
is editorinput()/editorrender()/editorlogic() in editor.cpp. In my
opinion, they should probably be split off into their own separate file
because editor.cpp is getting way too big, but this will do for now.
This is a refactor that simply moves all temporary variables off of
entityclass, and makes it so they are no longer global variables. This
makes the resulting code easier to understand as it is less entangled
with global state.
These attributes were:
- colpoint1
- colpoint2
- tempx
- tempy
- tempw
- temph
- temp
- temp2
- tpx1
- tpy1
- tpx2
- tpy2
- temprect
- temprect2
- x (actually unused)
- dx
- dy
- dr
- px
- py
- linetemp
- activetrigger
- skipblocks
- skipdirblocks
Most of these attributes were assigned before any of the times they were
used, so it's easy to prove that ungloballing them won't change any
behaviors. However, dx, dy, dr, and skipblocks are a bit more tricky to
analyze. They relate to blocks, with dx, dy, and dr more specifically
relating to one-way tiles. So after some testing with the quirks of
one-way tiles, it seems that the jankiness of one-way tiles haven't
changed at all, either.
Unfortunately, the attribute k is clearly used without being assigned
beforehand, so I can't move it off of entityclass. It's the same story
with the attribute k that Graphics has, too.
This prevents users from being confused whenever they type a pipe in the
script editor, then save the level and load it again and see their
script lines unexpectedly splitting in two. Now if you attempt to type a
pipe, it simply won't happen at all.
Fixes#379.
It's possible that SDL_atoi() could call the libc atoi(), and if a
string is provided that's too large to fit into an integer, then that
would result in undefined behavior. To avoid this, use SDL_strtol()
instead.
Instead of copying to a temporary string, just use SDL_strncmp(). Also,
I checked the blame, and apparently I committed the line that used
strcmp() instead of SDL_strcmp(), for whatever reason. But that's fixed
now.
For some reason, the variable `k` is on entityclass and gets mutated in
createentity() and createblock(). Then updateentities() uses it without
checking if it's valid, because either `k` or the size of `entities`
could have changed in the meantime. To fix any potential undefined
behavior, these bounds checks should be added.
This fixes a bug where font_positions wouldn't get cleared after exiting
a custom level that had a font.txt if it didn't exist in the default
graphics, leading to messed-up-looking font rendering.
This bug was originally discovered by Ally.
You're intended to rescue Violet first, and not second, third, or
fourth, and especially not last.
If you rescue her second, third, or fourth, your crewmate progress will
be reset, but you won't be able to re-rescue them again. This is because
Vitellary, Verdigris, Victoria, and Vermilion will be temporarily marked
as rescued during the `bigopenworld` cutscene, so duplicate versions of
them don't spawn during the cutscene, and then will be marked as missing
later to undo it.
This first issue can be trivially fixed by simply toggling flags to
prevent duplicates of them from spawning during the cutscene instead of
fiddling with their rescue statuses.
However, there is still another issue. If you rescue Violet last, then
you won't be warped to the Final Level, meaning you can't properly
complete the game. This can be fixed by adding a `crewrescued() == 6`
check to the Space Station 1 Level Complete cutscene. There is
additionally a temporary unrescuing of Violet so she doesn't get
duplicated during the `bigopenworld` cutscene, and I've had to move that
to the start of the `bigopenworld` and `bigopenworldskip` scripts,
otherwise the `crewrescued() == 6` check won't work properly.
I haven't added hooks for Intermission 1 or 2 because you're not really
meant to play the intermissions with Violet (but you probably could
anyway, there'd just be no dialogue).
Oh, and the pre-Final Level cutscene expects the player to already be
hidden before it starts playing, but if you rescue Violet last the
player is still visible, so I've fixed that. But there still ends up
being two Violets, so I'll probably replace it with a special cutscene
later that's not so nonsensical.
I have no idea why neither conveyors and moving and disappearing
platforms rendered in the editor or in-game use
Graphics::drawentcolours(), but this needs to be bounds-checked just as
I did for the in-game rendering function.
The entity getters I'm referring to are entityclass::getscm(),
entityclass::getlineat(), entityclass::getcrewman(), and
entityclass::getcustomcrewman().
Even though the player should always exist, and the player should always
be indice 0, I wouldn't want to make that assumption. I've been wrong
before.
Also, these functions returning 0 lull you into a false sense of
security. If you assume that commands using these functions are fine,
you'll forget about the fact that `i` in those commands could be
potentially anything, given an invalid argument. In fact, it's possible
to index createactivityzone(), flipgravity(), and customposition()
out-of-bounds by setting `i` to anything! Well, WAS possible. I fixed it
so now they can't.
Furthermore, in the game.scmmoveme block in gamelogic(), obj.getplayer()
wasn't even checked, even though it's been checked in all other places.
I only caught it just now because I wanted to bounds-check all usages of
obj.getscm(), too, and that game.scmmove block also used obj.getscm()
without bounds-checking it as well.
When this is done, there is potential for a mistake to occur when
writing out the bounds check, which is eliminated when using the macro
instead. Luckily, this doesn't seem to have happened, but what's even
worse is I hardcoded 400 instead of using SDL_arraysize(ed.level), so if
the size of ed.level the bounds checks would all be wrong, which
wouldn't be good. But that's fixed now, too.
This is because if they are manually written out, they are more likely
to contain mistakes.
In fact, after further review, there are several functions with
incorrect manually-written bounds checks:
* entityclass::entitycollide()
* entityclass::removeentity()
* entityclass::removeblock()
* entityclass::copylinecross()
* entityclass::revertlinecross()
All of those functions forgot to do 'greater than or equal to' instead
of 'greater than' when comparing against the size of the vector. So they
were erroneous. But they are now fixed.
It's better to do INBOUNDS_VEC(i, obj.entities) instead of 'i > -1'.
'i > -1' is used in cases like obj.getplayer(), which COULD return a
sentinel value of -1 and so correct code will have to check that value.
However, I am now of the opinion that INBOUNDS_VEC() should be used and
isn't unnecessary.
Consider the case of the face() script command: it's not enough to check
i > -1, you should read the routine carefully. Because if you look
closely, you'll see that it's not guaranteed that 'i' will be initialized
at all in that command. Indeed, if you call face() with invalid
arguments, it won't be. And so, 'i' could be something like 215, and
that would index out-of-bounds, and that wouldn't be good. Therefore,
it's better to have the full bounds check instead of checking only one
bounds. Many commands are like this, after some searching I can also
name position(), changemood(), changetile(), changegravity(), etc.
It also makes the code more explicit. Now you don't have to wonder what
-1 means or why it's being checked, you can just read the 'INBOUNDS' and
go "oh, that checks if it's actually inbounds or not".
For some reason it called obj.getplayer() and did nothing with the
result. Weird. But it does say "Test script for space station" above.
Removing this fixes an 'unused variable' warning.
With the scope of these variables reduced, it makes analyzing this
function easier, as you can now clearly see all temporary variables are
actually initialized before they're used.
Since there's an INBOUNDS_ARR() macro, it's much better to specify the
macro for the vector is a macro for the vector, to avoid confusion.
All usages of this macro have been renamed accordingly.
Stuck prevention (pushing the player/supercrewmate out if they are
inside a wall) has been factored out into its own function, so it's no
longer copy-pasted but slightly tweaked just for the supercrewmate.
Instead of having two separate functions to move entities along vertical
moving platforms, one for the player and one for the supercrewmate, they
have been consolidated into one function.
In-level, they were made to be gray in #323. The editor does not reflect this however; they're still shown as
green. For the same reasons in #323, this adds special cases to draw the entities as gray.
Closes#372.
Also, I changed my name in contributors.txt to be my username as I didn't feel comfortable with it being my name.
Co-authored-by: Misa <ness.of.onett.earthbound@gmail.com>
The game has four different functions for the same XML schema:
Game::loadtele(), Game::savetele(), Game::loadquick(), and
Game::savequick(). This essentially means one XML schema has been
copy-pasted three different times.
I can at least trim that number down to being copy-pasted only one time
by de-duplicating the reading and writing part. So both Game::loadtele()
and Game::loadquick() now use Game::readmaingamesave(), and
Game::savetele() and Game::savequick() now use
Game::writemaingamesave().
This will make it take less work to add XML forwards compatibility
(#373).
Due to #464, standing inside a gravity line during a gotoroom that
occurs every frame will end up with the gravity line being gray instead
of being white. To temporarily fix this (until #464 is properly fixed),
I decided to add some kludge that colors it white if its onentity is 1.
I tested this patch with gravity lines in both constant-gotoroom and
normal environments, and it seems to be fine for both.
In 2.0, 2.1, and 2.2, calling flipgravity() on an entity that wasn't
rule 6 would change it to rule 7. In 2.3 currently, doing this will only
change it to rule 7 if it's already rule 6, starting with the
introduction of the change where if an entity was rule 7 it would be
changed to rule 6.
The crewmate conversion trick has been restored, but converting an
entity to a crewmate will change its rule to 6, not 7 like in pre-2.3.
If you want it to be changed to rule 7 instead of 6, you'd have to call
flipgravity() twice in 2.3 and only once in pre-2.3, which would make
maintaining compatibility between versions a bit harder.
So to fix this, I'm inverting it so that if you call flipgravity() on an
entity that isn't rule 7, it will be converted to rule 7, and only if
it's rule 7 will it be converted to rule 6.
This is a followup to b7cf6855b0 and
10ed0058fd.
In 2.2, if you had a duplicate player entity, there'd be no way to get
rid of it. Except for the recently-discovered Arbitrary Entity
Manipulation glitch, where you set `i` to the indice of the entity and
call flipgravity() on it, turning its rule to 7 and making it no longer
a player entity.
However, I patched this useful mechanic out when I made it so that you'd
no longer be able to convert entities with rule 0 to rule 6
(10ed0058fd, upheld in
b7cf6855b0), because doing so would mean
being able to softlock the game by not having any player entity.
So, in this patch, I'm making it so that you CAN convert duplicate
player entities to crewmates (and thus basically destroy them), but you
can't do that to the TRUE player entity (i.e. the first entity indice
that has rule 0, which is basically always indice 0).
This patch fixes a regression caused by commit
6b1a7ebce6.
When you spawn a crewmate with an invalid color, by doing something like
`createentity(100,100,18,-1,0)` (here the color is -1, which is
invalid), a white crewmate with the color of solid white (255, 255, 255)
would appear.
That is, until AllyTally came along and committed commit
6b1a7ebce6 (Make "[Press ENTER to
return to editor]" fade out after a bit) (PR #158). Then after that
commit, it would seem like the crewmate didn't appear, but in reality
they were just invisible, because they had an invisible color.
As part of Ally's changes, to properly support drawing text with a
certain amount of alpha, she made BlitSurfaceColoured() account for the
alpha of the color given instead of only caring about the RGB of the
color, discarding the alpha, and using the alpha of the surface it was
drawing instead. This effectively made it so the alpha of whatever it
was drawing would be 255 all the time, except for if you had custom
textures and your custom textures had translucent pixels.
However, the default color set by Graphics::setcol() if you didn't
provide a valid color index was 0xFFFFFF. Which is only (255, 255, 255)
but ends up having an alpha value of 0 (because it's actually
0x00FFFFFF). And all colors drawn with alpha 0 end up being drawn with
alpha 0 after 6b1a7ebce6. So
invalid-colored entities will end up being invisible.
To fix this, I just decided to add alpha to the default value instead.
In addition, I used getRGB() to be consistent with all the other colors
in the function.
This is a regression from 25779606b4
(PR #74).
On the one hand, I should've thought this through carefully when
implementing the fix for the lower-score overwrite bug. On the other
hand, flibit didn't seem to notice either. And on the third hand, no one
else seems to have noticed, when they have had over 8 months to do so.
Not even the person who originally reported the lower-score overwrite
bug and also reported other bugs I hadn't noticed (e.g. the "You have
rescued a crewmate!" in Flip Mode) noticed this bug, which I believe was
weee50. But to be fair, he does seem to be less active nowadays.
On the fourth hand, I only realized the cause of the duplicate bug after
stepping through it in GDB, instead of just looking at it and going "hey
wait a minute" earlier. I'm surprised it didn't take me longer to
realize the problem.
I'm not sure what all these hands mean anymore, or where I'm getting
extra hands from. Whatever. This regression is fixed now.
So, I was staring at VVVVVV code one day, as I usually do, and I noticed
that warp lines had this curious code in entityclass::updateentities()
that set their statedelay to 2, and I thought, hm, maybe the pre-2.1
warp line bypass is caused by this statedelay. And, it doesn't seem like
this is the primary code used to detect if the player collides with warp
lines, the actual code is commented with "Rewritten system for mobile
update" and bolted-on in gamelogic() instead of properly being in
entityclass::entitycollisioncheck().
So, after getting tripped up on the misleading indentation of that
"Rewritten system" block, I removed the rewritten system, re-added
collision detection for rule 7 (horizontal warp lines), and after
checking the resulting behavior, it appears to be nearly identical to
that of warp lines in 2.0.
You see, if you use warp lines to flip up from the top of the screen
onto the bottom of the screen, close to the edge of the bottom of the
screen, Viridian's head will display on the top of the screen in 2.0. In
2.1 and later, this doesn't happen, confirming that my theory is
correct. I also performed warp line bypass multiple times in 2.0 and
with my restored code, and it is pretty much the exact same behavior.
So now, the pre-2.1 warp line bypass glitch has been re-enabled in
glitchrunner mode.
This misleading indentation makes it really easy to misanalyze this
block as only containing the statements up to obj.customwarplinecheck(),
when in reality it contains everything up to the modifying of
map.warpx/map.warpy. I have made this misanalysis just now in my attempt
to figure out the pre-2.1 warp line bypass glitch, and I don't like it.
So I'm fixing this indentation now.
So there's this trick that I recently discovered, since many script
commands don't initialize `i` it's possible to use them to manipulate
arbitrary entities by specifying their indice.
This means in 2.2 you can convert entities to pseudo-crewmates by
changing their rule to 6. Except in 2.3, this was fixed when I fixed the
command to work on flipped crewmates as well. So I'm restoring this
functionality, but I recognize the protection that my previous change to
the command did in preventing levels from destroying the player entity
by changing the player's rule to something nonzero, so instead of
removing the if-conditional entirely, I'm making it so that it will only
set the rule if the entity's rule isn't 0.
For some reason, GetSubSurface() and ApplyFilter() were hardcoding the
bits-per-pixel and/or mask arguments to SDL_CreateRGBSurface(). I've
made them simply re-use the bits-per-pixel and masks of the input
surfaces they operate on, but the bits-per-pixel should always be 32 and
masks should always go first-byte alpha, second-byte red, third-byte
green, fourth-byte blue.
The game usually runs on little-endian anyways, but even if it did run
on big-endian, it doesn't check endianness everywhere so these checks
are useless. Furthermore, the code should be written so that endianness
doesn't matter in the first place.
Neither of these were used anywhere, so to simplify the code and prevent
having potentially broken code that's never shown to be broken because
it's never tested, I'm removing these.
For some reason, there's some color-clamping code in this function
directly after already-existing color-clamping code. This code dates
back to 2.2. And also, there's a smaller lower-bound of -1 for the red
channel, despite the fact that this smaller value doesn't matter because
the red would get clamped to 0 by the first code anyway.
So even if this was put here for some strange reason, it doesn't matter
because it doesn't do anything anyway.
It's trivially easy to send the scripting system into an infinite loop
on the same frame (i.e. without any script delay in between, i.e. within
the same execution of script.run()). Just take a look at these two
scripts:
a:
iftrinkets(0,b)
#
b:
iftrinkets(0,a)
#
The hashes are to prevent the scripting system from parsing iftrinkets()
using the internal version instead of the simplified version, because
after doing a simplified iftrinkets(), the parser will (to oversimplify)
execute the last line of the script as internal.
Anyway, sending the game into an infinite loop like this will cause the
Not Responding dialog on Windows.
So to prevent this from happening, I've added an execution counter to
scriptclass::run(). If it gets too high, we're in an infinite loop and
so we stop running the script.
I noticed that if I have a large amount of entities in the room, the
game starts to freeze and one frame would take a very long time. I
identified an obvious cause of this, which is that the entity collision
checking in entityclass::entitycollisioncheck() is O(n²), n being the
number of entities in the room.
But it doesn't need to be O(n²). The only entities you need to check
against all other entities are the player and the supercrewmate. You
don't need to "test entity to entity" if 99% of the pairs of entities
you're checking don't involve the player or supercrewmate.
How do we make it O(n)? Well, just hoist the rule 0 and type 14 checks
out of the inner for-loop. That way, the inner for-loop won't be
unconditionally ran, meaning that in most cases it will always be O(n).
However, if you start having large amounts of duplicate player or
supercrewmate entities (I don't know why you would), it would start
approaching O(n²), but I feel like that's fair in that case. But most of
the time, it will be O(n).
So that's how collision checking is now O(n) instead.
There's not really any good reason to prevent this action during a fade
animation. That just makes the fade timer one more potential contributor
to a softlock.
I'm leaving the fademode conditional on the Time Trial quick restart,
though - removing it would mean being able to quick restart during a
fade-in, and thus being able to spam Enter over and over to keep
re-starting the fade animation, which looks goofy.
The hooks to bring up the map screen, pause screen, quit from Super
Gravitron, restart Time Trial, and commit suicide have now been hoisted
out of the for-loop that checked for a player entity. None of these
actions require a player entity, and there's no good reason to take away
your control from any of these actions, especially being able to quit to
the menu. The only actions inside the for-loop now are activating a
terminal and activating a teleporter, both of which require a player
entity to be standing in front of a terminal or teleporter, and both of
which have good reasons to be temporarily disabled.
This makes it so the fix for crewmates' drawframes being wrong for
1-frame is fixed for all crewmates regardless of when they get created.
Sure, crewmates created in mapclass::loadlevel() have their drawframes
fixed there, but for crewmates that get created from scripting (such as
Violet when gotorooming to the Ship teleporter room after Space Station
1), this fix doesn't apply to them. But now it does, and Violet will no
longer be facing the wrong way for 1 frame when teleporting to the Ship
teleporter room in the Space Station 1 Level Complete cutscene.
If the player is stuck in a wall (which shouldn't happen in the first
place), their sprite would always default to being flipped, even if they
were unflipped.
Being stuck in a wall is characterized by having both positive onfloor
and onground.
It's perfectly acceptable to have both warp lines and a warping
background in the same room. Many levels do this exact thing, I would
say at least 30 or so levels, many of them popular and played by many,
and this has never caused any issues at all.
All that having both warp lines and warp BG does is make it so the
warping of the warping background gets overriden by the warp lines, but
make it so the background is still a warp background. So in effect, you
can have a warp background without any warping. This is perfectly
defined behavior. Except, for whatever reason, it's unintentional, and
the editor tries to prevent you from doing it.
Key word being "tries". The code to prevent having both warp types is
bugged (at least when you change the warp BG. The check when you place
warp lines seems to be solid). It compares the p1 and p2 attributes of
warp lines to the x-coordinate and y-coordinate of the room, despite p1
and p2 having nothing to do with room coordinates. p1 is the type of the
warp line and should be treated as an enum, and p2 is the offset of the
warp line from the top/left of the screen. This results in this check
sometimes working if you're unlucky, but never actually working properly
most of the time. This means people can first place warp lines, and then
change the warp background later, to have both warp lines and a warp
background.
Having these checks just further complicates the code, makes it more
error-prone, and just inconveniences people when they want to do
something that's perfectly fine to do. So it's best if we just remove
these checks.
Fixes#402 (Violet appearing 1 frame after the Ship teleporter room
appears).
The root cause of this bug is due to the game loop order changes made
with the over-30-FPS patch. 2.2's game loop order was gameinput(),
gamerender(), then gamelogic(). In 2.3, gamerender() was moved to the end
as it required special code to render more than 30 frames a second. So
2.3's game loop order is gameinput(), gamelogic(), then gamerender().
In hindsight, I could have preserved the game loop order, but this would
require some more complex code in the game loop than what is there
currently. Fixing it now would fix rendering glitches such as this one
(along with being able to remove script.dontrunnextframe with the
two-frame-delay fix), but it could also introduce new rendering glitches
we don't yet know about. After discussing this in Discord DMs with
flibit, we agreed that the game loop order should be fixed in 2.4
instead.
When the game teleports you, gamelogic() runs script.teleport(). This
function will gotoroom to the teleporter destination, then it loads the
teleport script. Some teleport scripts (such as levelonecomplete, which
creates Violet) expect that their entities will be created, and more
generally that their script will be ran, on the same frame that the
gotoroom happens, i.e. by the time that the next gamerender() happens,
i.e. script.run() should be ran before the next gamerender() happens.
This would be true on the old game loop order, but with the new game
loop order, gamerender() gets ran directly after gamelogic() with no
script.run() in between.
To fix this, I did the same thing I did with the two-frame-delay fix
(#317), where I ran the script for that frame, but in order to prevent
running it twice I set script.dontrunnextframe to true.
This is a follow-up to #421.
The game would draw the activity zone if there was one active at all,
and would ignore game.act_fade. Normally this wouldn't be a problem, but
since #421, it's possible that there could be an active activity zone
but game.act_fade would still be 5. This would happen if you entered a
new activity zone while a cutscene was running.
To fix this, just make it so that the prompt gets drawn on its own and
only depends on the state of game.act_fade (or game.prev_act_fade), and
won't be unconditionally drawn if there's an active activity zone. As a
bonus, this de-duplicates the drawing of the activity prompt, so it's no
longer copy-pasted.
This fixes a bug where opening a script, then closing the script but not
exiting the script list, then opening a script again (it can be the same
script as before) would result in you being unable to type in the
script. You would have to close the script, then close the script list,
then re-open the script list in order to be able to type properly.
This was caused by the fact that key.enabletextentry() was being called
when the script list was opened, but key.disabletextentry() was being
called when closing a script. So the fix for this is to simply move the
key.enabletextentry() to its proper place, which is when the script is
being opened, and not when the script list is.
Some levels rely specifically on the fact that certain script boxes are
loaded using gamestates, instead of directly loading the script and
bypassing the gamestate system. Then weird things could happen. This
restores compatibility with those levels.
mapclass::twoframedelayfix() doesn't need to be updated because the
point of that function is to bypass the gamestate system entirely
anyways.
There's not really any need for it to be there. It gets called when the
Time Trial restarts, as restarting the Time Trial calls
script.startgamemode(), which calls script.hardreset() anyway.
Furthermore, since script.hardreset() is removed, we can also remove two
lines that are meant to work around the fact that everything gets reset,
which is now no longer the case.
Fixes#367.
Previously, it was assuming that the number of PPPPPP/MMMMMM tracks
would always be 16, since if that wasn't the case... then the game would
rudely and abruptly segfault when attempting to load the file. Huh.
But now that the game properly deals with invalid headers, it's possible
for the number of tracks to be 0. So I'll need to remove this
assumption.
It's possible that musicReadBlob.getIndex() could return the sentinel
value of -1 in case the header with that name is invalid, in which case
we should simply not do anything. Otherwise it'll lead to segfaults. I
opted to do the full bounds check just to be safe, too.
For further safety, I hardcoded the max number of headers, 128, less, so
128 is copy-pasted less and in the future if it needs to be changed
it'll only have to be changed in one place.
The binary blob shouldn't return an index if it ends up being invalid.
That could cause a whole lot of issues if musicclass ends up parsing the
resulting struct.
With all that said, though, musicclass doesn't check the -1 sentinel
value anyway, even though it should, but that'll be fixed later.
This fixes the bug where in glitchrunner mode, quitting to the menu
would always put you back at the play menu on the first option, instead
of the menu you entered the game from.
The problem is the script.hardreset() that gets called before the game
actually quits to the menu, so when Game::quittomenu() gets called to
quit to the menu, all the variables that keep track of whether you're in
a certain gamemode, such as game.insecretlab and map.custommode, all get
prematurely reset before that function can read them and put you back to
the correct menu.
The solution here is to simply reset only what's needed when quitting to
the menu. Specifically, in order for credits warp to work,
script.running needs to be set to false and all the text boxes need to
be removed. Text boxes need to be gone so the "- Press ACTION to advance
text -" prompt will stay up without a text box, enabling the player to
increment the gamestate at will by pressing ACTION, and the script needs
to stop running so further text boxes don't spawn in.
Fixes#389.
After looking at pull request #446, I got a bit annoyed that we have TWO
variables, key.textentrymode and ed.textentry, that we rolled ourselves
to track the state of something SDL already provides us a function to
easily query: SDL_IsTextInputActive(). We don't need to have either of
these two variables, and we shouldn't.
So that's what I do in this patch. Both variables have been axed in
favor of using this function, and I just made a wrapper out of it, named
key.textentry().
For bonus points, this gets rid of the ugly NO_CUSTOM_LEVELS and
NO_EDITOR ifdef in main.cpp, since text entry is enabled when entering
the script list and disabled when exiting it. This makes the code there
easier to read, too.
Furthermore, apparently key.textentrymode was initialized to *true*
instead of false... for whatever reason. But that's gone now, too.
Now, you'd think there wouldn't be any downside to using
SDL_IsTextInputActive(). After all, it's a function that SDL itself
provides, right?
Wrong. For whatever reason, it seems like text input is active *from the
start of the program*, meaning that what would happen is I would go into
the editor, and find that I can't move around nor place tiles nor
anything else. Then I would press Esc, and then suddenly become able to
do those things I wanted to do before.
I have no idea why the above happens, but all I can do is to just insert
an SDL_StopTextInput() immediately after the SDL_Init() in main.cpp. Of
course, I have to surround it with an SDL_IsTextInputActive() check to
make sure I don't do anything extraneous by stopping input when it's
already stopped.
All that pressing R does in No Death Mode is end your run. As a result,
it'll only be pressed by accident, so it's better to just disable it
instead.
It's not even useful to quick-restart, because it's faster to quit and
go through the menu again than it is to wait through the Game Over
screen.
Additionally, I removed the `game.deathseq<=0` conditional because it's
unnecessary due to the if-statement already being inside a
`game.deathseq == -1` conditional.
strcat()s and strcpy()s have been replaced with SDL_snprintf() where
possible, to clearly convey the intent of just building a string that
looks a certain way, instead of spanning it out over multiple lines.
Where there's not really a good way to avoid strcat()/strcpy() (e.g. in
PLATFORM_getOSDirectory()), they will at least be replaced with
SDL_strlcat() and SDL_strlcpy(), which are safer functions and are less
likely to have issues with null termination.
I decided not to bother with PLATFORM_migrateSaveData(), because it's
going to be axed in 2.4 anyways.
There's no need to call a string function and have function call
overhead if you remember how C strings work: they have a null
terminator. So if the first char in a string is a null terminator, then
the string is completely empty. So you don't need to call that function.
The previous check by mwpenny had a few issues:
(a) It was completely overcomplicated for no good reason, and was
basically a Rube Goldberg machine. The original check was...
(1) Creating an std::string of the last char of 'output'...
(2) ...except instead of using the normal std::string constructor, it
was using the one where you pass in a number and a char to create
a string that's just that char repeated N times... except this
was only used to create a 1-length string.
(3) Converted that std::string to a C string.
(4) Then passed it to strcmp(), despite the string at this point
being only one byte and you could just compare the char values
directly.
The original check could've just been:
output[SDL_strlen(output) - 1] == *pathSep
(b) Use of libc strcmp() and strlen() instead of SDL_strcmp() and
SDL_strlen().
Now, actually, PHYSFS_getDirSeparator() happens to be a char array and
not a single char, so mwpenny was going in the right direction by using
strcmp() after all. Except it doesn't seem like he thought about the
fact that PHYSFS_getDirSeparator() could be multiple bytes instead of
one, and so he ended up making the first argument to strcmp() always be
a one-byte char array.
So there's issue (c), which is that it assumes the path separator is one
byte instead of multiple.
This commit fixes all of these issues with the trailing path separator
check.
If necessary, the caller can provide a fallback to be returned in case
the input given isn't a valid integer, instead of having to duplicate
the is_number() check.
This is simply a wrapper function around SDL_atoi(), because SDL_atoi()
could call libc atoi(), and whether or not invalid input passed into the
libc atoi() is undefined behavior depends on the given libc. So it's
safer to just add a wrapper function that checks that the string given
isn't bogus.
std::isdigit() can be replaced with SDL_isdigit(), but there's no
equivalent for std::isxdigit() in the SDL standard library. So we'll
just use libc isxdigit() instead, it's fine.
When I added the over-30-FPS mode, I kept running into this problem
where the special images of text boxes would render during the
deltaframes of fade-in/fade-out animations, even though they shouldn't
be. So I simply added a flag to the text box that enables drawing these
special images.
However, this doesn't solve the problem fully, and there's still a small
chance that a special-image text box could draw another special image
during its deltaframes. It's really rare and you have to have your
deltaframe luck juuuuuust right (or you could use libTAS, probably), but
it helps to be in 40% slowmode and have a high refresh rate (which, if
it isn't a multiple of 30, you should disable VSync, too, in order to
not have a low framerate).
So instead, special images will only be drawn if the text box has fully
faded in completely. That solves the issue completely.
This commit fixes a bug that's existed since MMMMMM was added (so, 2.2),
where if you quicksaved in a custom level while no music was playing,
then quit and loaded that quicksave, and you were using PPPPPP while
having MMMMMM available, it would play MMMMMM track 15, even though the
game intends for the music to simply be silent.
This is due to the same bug that lets you play MMMMMM tracks if you're
on PPPPPP - musicclass::play() does a modulo, but C++ modulo is not
guaranteed to be positive given negative inputs, so the 16-track offset
is added to a negative number, resulting in targeting the MMMMMM
soundtrack instead of PPPPPP.
That exploit doesn't harm anyone and shouldn't be fixed, EXCEPT it
causes a problem in this specific case. But this bug can be fixed
without removing that exploit.
Note that I made the check do not-equal to -1 instead of greater-than
-1, so levels that intend on using track -2, -3, -4, etc. upon loading a
quicksave will still work as their creator intended. It's just that
specifically -1 is patched out, just to fix this issue.
For some reason, ScaleSurface() was drawing each pixel one pixel up and
to the left from its actual position. I have no idea why.
But this was causing an issue where pixels would just be dropped,
because they would be drawn outside of the temporary SDL_Surface from
which the scaled surface would be drawn onto. Also, the offset just
creates a visual one-pixel offset in the result for no reason. So I'm
just removing it.
Big Viridian also suffered from this one-pixel offset, so now they will
no longer look like they're floating above the ground when standing on
the floor.
ScaleSurfaceSlow() uses DrawPixel() instead of SDL_FillRect() to scale a
given surface, which is slow and inefficient, and makes it less likely
that the game could be moved to SDL_Render.
Unfortunately, it has this weird -1,-1 offset, but that will be fixed in
the next commit.
So, earlier in the development of 2.0, Simon Roth (I presume)
encountered a problem: Oh no, all my backgrounds aren't appearing! And
this is because my foregroundBuffer, which contains all the drawn tiles,
is drawing complete black over it!
So he had a solution that seems ingenius, but is actually really really
hacky and super 100% NOT the proper solution. Just, take the
foregroundBuffer, iterate over each pixel, and DON'T draw any pixel
that's 0xDEADBEEF. 0xDEADBEEF is a special signal meaning "don't draw
this pixel". It is called a 'key'.
Unfortunately, this causes a bug where translucent pixels on tiles
(pixels between 0% and 100% opacity) didn't get drawn correctly. They
would be drawn against this weird blue color.
Now, in #103, I came across this weird constant and decided "hey, this
looks awfully like that weird blue color I came across, maybe if I set
it to 0x00000000, i.e. complete and transparent black, the issue will be
fixed". And it DID appear to be fixed. However, I didn't look too
closely, nor did I test it that much, and all that ended up doing was
drawing the pixels against black, which more subtly disguised the
problem with translucent pixels.
So, after some investigation, I noticed that BlitSurfaceColoured() was
drawing translucent pixels just fine. And I thought at the time that
there was something wrong with BlitSurfaceStandard(), or something.
Further along later I realized that all drawn tiles were passing through
this weird OverlaySurfaceKeyed() function. And removing it in favor of a
straight SDL_BlitSurface() produced the bug I mentioned above: Oh no,
all the backgrounds don't show up, because my foregroundBuffer is
drawing pure black over them!
Well... just... set the proper blend mode for foregroundBuffer. It
should be SDL_BLENDMODE_BLEND instead of SDL_BLENDMODE_NONE.
Then you don't have to worry about your transparency at all. If you did
it right, you won't have to resort this hacky color-keying business.
*sigh*
For some reason, the cursor would be either disabled and re-enabled if
you switched to windowed mode, or it would be always enabled if you
switched to fullscreen mode. This only happened when you toggled
fullscreen using the Alt+Enter, Alt+F, or F11 keybinds, and the
fullscreen option in graphic options doesn't have this problem.
This cursor toggling business seems like an arcane incantation back in
the days of SDL1.2, now since no longer necessary with SDL2. However,
after some testing, it seems like removing these indecipherable runes
don't cause any harm, so I'm going to remove them.
Fixes#371.
This function checked the intersection of rectangles, but it used floats
for some reason when its only caller used ints. There's already an
intersection function (UtilityClass::intersects()), so we should be
using that one instead in order to minimize code duplication.
Graphics::Hitest() is used for per-pixel collision detection, directly
checking the pixels of both entities' sprites. I checked with one of my
TASes and it still syncs, so I'm pretty sure this won't cause any
issues.
If you have the translucent room name option enabled, you'd always be
seeing the spikes at the bottom of the screen hidden behind the room
name. This patch makes it so that the spikes get carefully cropped so
they only appear above the room name when the player gets close to the
bottom of the screen.
The function was not actually checking the number that would end up
being used to index the tiles3 vector, and as a result there could
potentially be out-of-bounds indexing. But this is fixed now.
The half-second delay comes from the fact that the game uses
graphics.resumegamemode to go back to GAMEMODE. This system waits for
graphics.menuoffset to reach a certain threshold before actually going
back (this is the animation of the map screen being brought down). To
speed it up, I'll just set graphics.menuoffset directly.
I could've directly set game.gamestate to GAMEMODE, but I wanted to be
safe and use the existing system instead.
This removes the arrays of zeroes that still take up space in the
binary. It doesn't seem like the compiler optimizes or compresses these
zeroes anyway, so just remove them instead, and make
load()/loadminitower()/loadminitower2() no-op functions. The
minitower/contents arrays are already initialized zeroed-out, anyway, so
there's no need to keep these other arrays around.
This saves exactly 72 kilobytes of memory and binary size.
This moves the teleporter, activity prompt, and trophy text "don't draw"
conditionals to the part where the game checks collision with them,
instead of whenever the game draws them.
This makes it so that the game smoothly does the fade-in/fade-out
animation instead of suddenly stopping rendering them whenever their
"don't draw" conditions apply. Now, the "Press ENTER to activate
terminal" prompt will no longer suddenly disappear whenever you activate
one, and the "- Press ENTER to Teleport -" prompt will smoothly fade
back in after teleporting, instead of suddenly popping in on screen.
When I refactored custom level entity creation logic earlier, I forgot
to account for enemy bounds, so enemies would take on the same bounds as
platforms. But this is fixed now.
When I compile in release mode, GCC complains that these variables MAY
be uninitialized when it comes time to create the moving platform
entity. I can't think of any way that it could be uninitialized and
testing my release build seemed to be fine, but I'm initializing these
variables just to be sure.
M&P contains network code despite M&P not being a Steam/GOG release (as
Steam/GOG releases are full releases of the game, not
custom-levels-only releases).
While unlocking achievements is already ifdef'd out in M&P, let's remove
the network code entirely to make sure people can't do other shenanigans
with M&P builds, and also to have a smaller binary size.
This fixes a possible graphical issue where the "Press ENTER to activate
terminal" prompt gets drawn on top of the "- Press ACTION to advance
text -" prompt, which looks bad. Trophy text gets drawn on top, too, so
there's a check there as well.
I've also made it so the activity prompt doesn't get drawn if the player
doesn't have control. After all, what use is it to say "press ENTER" if
the player isn't allowed to?
The game time is moved a little to the left, and the trinket count a
little to the right. To fix#376 for real, the trinket count is now
positioned automatically based on its length. The trinket icon is now
also displayed at the far right (instead of to the left of the count)
for better symmetry, and so that switching between tele save and quick
save doesn't make the trinket icon move if the trinket counts have
different lengths.
While I'm going to fix#376 anyway to make longer numbers (like Seventy
Eight) fit, I decided to also make the box a little wider in advance of
the game being localized, those extra chars could be a lifesaver, while
you wouldn't really notice the difference if you play the game in
English.
What this simply does is make it so that in the event that
game.hascontrol is somehow set to false when there isn't a cutscene
running (i.e. when game.state is 0 and script.running is false), it gets
set back to true again.
There's many ways to interrupt a gamestate and/or a running script, most
notably telejumping and doing a screen transition in the middle of the
animation, interrupting it.
This implements the first part of my idea in this comment:
https://github.com/TerryCavanagh/VVVVVV/issues/391#issuecomment-659757071
Achievements could be unlocked in custom levels/make and play, so this adds the wrapper function `game.unlockAchievement` which calls `NETWORK_unlockAchievement` if `map.custommode` is false.
Also, this function and `Game::unlocknum` have both been `ifdef`ed to be empty if MAKEANDPLAY is defined.
This makes the modifiers for removing tiles only work if you're using tools 0 or 1 (wall or background). In the future it might be worth it to add some extra subtools for spikes, but for now this should be fine.
Also, there was a small inconsistency with the tool drawn and the tool used. If you started holding down V, then started holding down Z, you would place tiles like you're holding V (9x9) but your cursor would look like you're holding Z (3x3). The `if` chain for drawing the resized cursors has been flipped around for this.
Otherwise, this would cause immense slowdown during the New Record
animation that plays when you die, as the game would be writing
unlock.vvv every frame.
To fix this, I just put it under the same if-guard as the
music.playef(), since the sound effect also only plays once. But I have
to watch out for frame ordering and make sure the record is actually set
before I call game.savestats(), and then of course I have to move
game.swnmessage = 1 over because that's the variable being checked in
the conditional.
There's a lot of places where the INBOUNDS() check is spelled out
instead of using the macro, before the macro was introduced. Also the
macro should probably be renamed INBOUNDS_VEC() to be consistent.
This fixes the bug where Viridian would appear to "zip" when they would
be teleported to the position of the teleporter before being flung out
of it.
As discussed in #393, I've also set the oldxp/oldyp when Viridian is
temporarily positioned in the center of the room, even though at this
point they should already be invisible. This is just to be safe.
Fixes#393.
Okay, so basically here's the include layout that this game now
consistently uses:
[The "main" header file, if any (e.g. Graphics.h for Graphics.cpp)]
[blank line]
[All system includes, such as tinyxml2/physfs/utfcpp/SDL]
[blank line]
[All project includes, such as Game.h/Entity.h/etc.]
And if applicable, another blank line, and then some special-case
include screwy stuff (take a look at editor.cpp or FileSystemUtils.cpp,
for example, they have ifdefs and defines with their includes).
It was never used and didn't do anything. It looks like it was intended
to be used but I guess time ran out or something, but it's too late now
and level files don't have timestamps or anything, so might as well just
remove it.
Good thing too, because asctime() is apparently deprecated.
Including a header file inside another header file means a bunch of
files are going to be unnecessarily recompiled whenever that inner
header file is changed. So I minimized the amount of header files
included in a header file, and only included the ones that were
necessary (system includes don't count, I'm only talking about includes
from within this project). Then the includes are only in the .cpp files
themselves.
This also minimizes problems such as a NO_CUSTOM_LEVELS build failing
because some file depended on an include that got included in editor.h,
which is another benefit of removing unnecessary includes from header
files.
That's how it should be done, because the SDL headers aren't going to be
installed in this repository. The game was a bit inconsistent before but
now it isn't anymore.
This removes around megabyte from the binary, so a stripped -Og binary
went from 4.0 megabytes to 2.9 megabytes, and an unstripped -O0 binary
went from 8.1 megabytes to 7.1 megabytes, which means I can now finally
upload an unstripped -O0 binary to Discord without having to give money
to Discord for their dumb Nitro thing or whatever.
There were many different ways I could've fixed it, but one thing that
stood out to me was the fact that touching the teleporter wasn't
guaranteed to set its onentity to 0, even though it should be. So now,
every time Viridian touches the teleporter, the teleporter's onentity
will be set to 0, and thus there's no chance of the teleporter
interrupting its own teleport animation and softlocking the game.
We should still do what I suggested in #391, namely setting
game.hascontrol to true if the game is in gamestate 0 and script.running
is false, and also always allowing Esc/Enter to be pressed regardless of
game.hascontrol. But this softlock is fixed now.
Fixes#391.
Whoops. Forgot to do this earlier when adding the Shift+F3 hotkey.
Otherwise the enemy type would become invalid and just turn into the
default square.
The game would softlock if you brought up the map screen or quit screen
after exiting the Super Gravitron to the Secret Lab. This softlock would
only happen if you were in glitchrunner mode.
This is because glitchrunner mode set game.fadetolabdelay when it
shouldn't have, and also checked game.fadetolabdelay when it shouldn't
have.
So I made it so that the game will only set game.fadetolabdelay when not
in glitchrunner mode (I already had a check for game.fadetomenudelay,
too!) and the game will only check for game.fadetomenudelay and
game.fadetolabdelay when not in glitchrunner mode, as well.
I originally made the game check game.fadetomenudelay and
game.fadetolabdelay to prevent being able to re-press ACTION to re-start
the fadeout if the game was already fading out. And I made sure that
this wasn't broken, both in glitchrunner mode and normal mode.
Whoops.
Noticed this earlier when I was merging upstream back into VCE, and
interestingly enough, it doesn't look like cppcheck warns about
undeffing a non-existent define.
I don't know who wrote this code originally, but it's extremely obvious
that it was a different person than who wrote the rest of the code.
Anyway, I fixed the spacing and braces so everything is smushed together
less.
Also, there's no need to put the entire warpdir checking in an
'if(room.warpdir>0)' statement. If any of the cases is jumped to, then
you already know that's true.
"Threadmills" are now properly called "conveyors". I don't know why they
were called "threadmills" anyway, the proper spelling is "treadmills".
Also, warp line `p1`s of 0 and 3 are now properly labeled, as well as
the trinket edentity.
Just set the map roomname to the roomname of the room. It's completely
redundant to set the roomname to an empty string and check if the
roomname of the room is empty.
I do this because we declare-and-initialize some variables in the case.
This isn't strictly necessary, since there's no cross-initialization
errors since it's the last case in the switch, but I'd just like to be
future-proof.
Instead of repeating 'ed.level[curlevel]' (or even worse, you type in
that giant expression inside the brackets instead of reusing
'curlevel'), why not just type 'room'? As an added bonus, I added bounds
checks so 'room' is always guaranteed to point to an existing object.
There are some levels that index 'ed.level' out of bounds, and I'd like
to make their behavior properly defined instead of being undefined. One
of the things usually true about out-of-bounds rooms is that they're
always tiles2.png (that means an edlevelclass tileset that isn't 0),
because the chance of the memory location being exactly 0 is smaller
than it being nonzero. So the out-of-bounds room has tileset 1.
Again, it used this severely overcomplicated expression for god knows
what reason. I've replaced it with a simpler one. Also it's const just
to indicate intent.
Previously it copy-pasted this god-awful overcomplicated expression
(possibly for cursed ActionScript legacy reasons?) everywhere, instead
of putting it in a variable, or even just using a simpler one like the
one I replaced it with.
Now the obj.createentity() and obj.createblock() calls are much nicer.
Now if your cursor was at the bottom of the screen when you entered
playtesting but then was moved to the top during playtesting, when you
exit, the roomname won't instantly disappear and then sheepishly rise
back up again.
And if your cursor was at the bottom of the screen when you entered
playtesting, and exited still being that way, the roomname will rise
back down again and won't instantly disappear.
Both of these behaviors make the roomname movement much more continuous
than it was previously, and it feels smoother and less janky.
Also, use inspecial() instead of writing out each part of the
conditional separately. This just basically adds the insecretlab
conditional to the if-statement, which shouldn't be a big deal.
There's no reason you shouldn't be allowed to press Enter on teleporters
during playtesting.
Well, except that you can press Esc in the teleporter menu in order to
go to the pause menu, which isn't intended in playtesting. But I can
just add a check there so that pressing Esc closes the teleporter menu
instead, it's fine.
graphics.textbox.clear() should be used instead of
graphics.textboxremove() or graphics.textboxremovefast(), because even
with graphics.textboxremovefast(), you'll still have to process at least
one frame of GAMEMODE logic before the text boxes are actually properly
removed, and this caused a 1-frame glitch when exiting playtesting with
text boxes on-screen and then re-entering playtesting.
Technically I could've only fixed it in Game::returntoeditor(), but I
wanted to be safe, so I also fixed it in scriptclass::hardreset(), too.
This fixes a bug where the settings menu would immediately be brought up
if you used Esc to exit playtesting, unless you were an incredible ninja
and only pressed it for exactly one frame.
This fixes an annoying bug where if you use Up or Down to press ACTION
on the "All crewmates rescued!" dialogue whenever you rescue the last
crewmate during playtesting, it'll move you to the room above or below
you. This is because ed.keydelay isn't set to 6 (which is the standard
value that it gets set to whenever you press most keys in the editor),
but now it is.
The shouldreturntoeditor variable is supposed to be used because it
fixes the warp background not being reset if you exit into a
horizontally/vertically warping room with a different background. It
also properly resets other variables, which is good, too.
Instead of using gamestates, just directly use the 'script' attribute of
a script box if it is non-empty.
This is accomplished by having to return the index of the block that the
player collides with, so callers can inspect the 'script' attribute of
the block themselves, and do their logic accordingly.
game.customscript is an unnecessary middleman, but it will be kept
around for compatibility reasons. However, it's still possible to crash
the game, so I'm adding this bounds check.
To avoid going through gamestates, we'll need to carry the name of the
script on the script box itself. And to do that, we'll need to set the
'script' attribute of script boxes when translating edentities into real
entities in custom levels.
This patch optimizes the loop used to limit the framerate in 30-FPS-only
mode so that it uses SDL_Delay() instead of an accumulator. This means
that the game will take up less CPU power in 30-FPS-only mode. This also
means that the game loop code has been simplified, so there's only two
while-loops, and only two places where game.over30mode is checked, thus
leading to easier-to-understand logic.
Using an accumulator here would essentially mean busywaiting until the
34 millisecond timer was up. (The following is just what leo60228 told
me.) Busywaiting is bad because it's inefficient. The operating system
assumes that if you're busywaiting, you're performing a complex
calculation and handles your process accordingly. And this is why
sleeping was invented, so you could busywait without taking up
unnecessary CPU time.
When I moved duplicate player entity removal to
scriptclass::hardreset(), I also inadvertently made it so all non-player
entities got removed as well, even though this wasn't my intent. And
thus, pressing Enter to restart a time trial removes every entity except
the player, since it calls script.hardreset().
The time trial script.hardreset() is bad for other reasons (see #367),
however it's still a good idea to reset only what's needed in
script.hardreset().
There's a bug where playtesting from Ved doesn't properly play the music
of the level, due to no fault with Ved.
This was because the music was being faded out by
scriptclass::startgamemode() case 23 after main() called music.play().
To fix this, just call music.play() when all the other variables are
being set in Game::customloadquick().
So you get a trophy and achievement for completing the game in Flip
Mode. Which begs the question, how does the game know that you've played
through the game in Flip Mode the entire way, and haven't switched it
off at any point? It looks like if you play normally all the way up
until the checkpoint in V, and then turn on Flip Mode, the game won't
give you the trophy. What gives?
Well, actually, what happens is that every time you press Enter on a
teleporter, the game will set flag 73 to true if you're NOT in Flip
Mode. Then when Game Complete runs, the game will check if flag 73 is
off, and then give you the achievement and trophy accordingly.
However, what this means is that you could just save your game before
pressing Enter on a teleporter, then quit and go into options, turn on
Flip Mode, use the teleporter, then save your game (it's automatically
saved since you just used a teleporter), quit and go into options, and
turn it off. Then you'd get the Flip Mode trophy even though you haven't
actually played the entire game in Flip Mode.
Furthermore, in 2.3 you can bring up the pause menu to toggle Flip Mode,
so you don't even have to quit to circumvent this detection.
To fix both of these exploits, I moved the turning on of flag 73 to
starting a new game, loading a quicksave, and loading a telesave (cases
0, 1, and 2 respectively in scriptclass::startgamemode()). I also added
a Flip Mode check to the routine that runs whenever you exit an options
menu back to the pause menu, so you can't circumvent the detection that
way, either.
The music for the Tower is supposed to be ecroF evitisoP in Flip Mode,
and Positive Force when not in Flip Mode. However, if you go to the
options from the pause menu and toggle Flip Mode, the music isn't
changed.
Fixing this is pretty simple, just check the current area if not in a
custom level and play the correct track accordingly when toggling Flip
Mode from in-game.
Gamestates 300..336 are used to start scripts in custom levels. However,
it looks like instead of having the cases have common code, each
individual case was copy-pasted numerous times, which is pretty
wasteful.
std::string is one of those special types that has a constructor that
just initializes itself to a blank state automatically. This means all
`std::string`s are by default already `""`, so there's no need to set
them. And in fact, cppcheck throws out warnings about performance due to
initializing `std::string`s this way.
I ran the game through cppcheck and it spat out a bunch of member
attributes that weren't being initialized. So I initialized them.
In the previous version of this commit, I added constructors to
GraphicsResources, otherlevelclass, labclass, warpclass, and finalclass,
but flibit says this changes the code flow enough that it's risky to
merge before 2.4, so I got rid of those constructors, too.
Looks like coins were basically a scrapped mechanic, although I'm not
sure what these attributes were for. I guess counting the number of
coins in each room? But why, when you can just make a function to count
them automatically? Whatever.
This is just in case these values happen to be used without being
initialized or anything. I vaguely recall someone reporting an issue
where they didn't have a "Documents" folder on Windows and their level
folder ended up being a garbage path, so it's good to do this.
There's a bug in the cutscene that plays if your companion is Vitellary
in the room "Now Stay Close To Me...". The relevant gamestate is
gamestate 43, which for Vitellary calls the script `int1yellow_4`.
When Vitellary says the text box "That big... C thing! I wonder what it
does?", Terry intended for Vitellary to change his facing direction to
the left, as you can see with the command `changedir(yellow,0)` in the
original scripting. `changedir()` just changes the `dir` attribute of an
entity, and a `dir` of 0 means face left and a `dir` of 1 means face
right.
Then when Vitellary says "Maybe we should take it back to the ship to
study it?", Terry intended for him to face rightwards once again, as
indicated by the `changedir(yellow,1)` command.
Unfortunately, what happens instead is that when Vitellary says the
first text box ("That big... C thing! I wonder what it does?"), he turns
left for precisely one frame, and then afterwards goes back to facing
right. Then the second text box comes around, but he's already facing
right. How come?
Well, the problem here is that Vitellary's AI for "follow Viridian" is
overriding his `dir` attribute. Vitellary's AI says "get close to
Viridian", but Vitellary is already close enough to them that he stays
put. However, he still turns to face them as part of that AI.
To fix that, we need to put him in the AI mode that specifically says to
face left, with the command `changeai(yellow,faceleft)`. That way, he no
longer has the AI mode of following Viridian, and he will actually look
left for the intended duration instead of only looking left for one
frame.
But then we have another problem. When the cutscene ends, Vitellary no
longer follows Viridian. I mean it makes sense - we just placed him in
"only face left" mode, not "follow Viridian" mode! And this is not
merely a visual problem, because Vitellary is a supercrewmate and the
game won't let the player walk off the screen if Vitellary isn't
offscreen yet.
To fix THAT issue, we'll need to put Vitellary back in "follow Viridian"
mode. It turns out that the `changeai()` command was more intended for
scripting crewmates (entity type 12), NOT supercrewmates (entity type
14). As such, the command assumes that you'll want state numbers that
apply to entity type 12, such as 10, 11, 12, 13, and 14, even though the
only one that applies to entity type 14 is state 0, and every other
state number just makes it so that the entity doesn't move an inch. And
specifying faceleft/faceright is just state number 17.
Luckily, we can still pass the raw state number to `changeai()`, we
don't have to use its intended names. So I do a `changeai(yellow,0)` to
set Vitellary's state number back to 0 when it comes time to make him
face right again.
As a bonus, I added comments to the changed lines. This is a semi-obtuse
method of scripting, so it's always good to clarify.
The spikes are removed if the game is in no death or time trial mode,
however the removal is accomplished by modifying a static array. So if
the player switches to no death or time trial mode and switches back to
regular play, the spikes will no longer be present until the game is
restarted.
This simple fix writes the spikes back to the static array if the game
is not in no death or time trial mode. Another option is to maintain 2
static arrays - one with the spikes and one without - but that is
needlessly wasteful and prone to mistakes (one array updated but not the
other).
For some reason, there were just exact duplicates of the talkgreen_2
script and alarmon/alarmoff commands. I have no idea why, but cppcheck
identified them.
Graphics::drawmenu() no longer has copy-pasted code for each individual
case. Instead, the individual cases have their own adding on to common
code, which is far easier to maintain.
Also, the only difference Graphics::drawlevelmenu() does is in some
y-positioning stuff. There's no reason to make it a whole separate
function and duplicate everything AGAIN. So it's been consolidated into
Graphics::drawmenu() as well, and I've added a boolean to draw a menu
this way if it's the level menu.
Instead, the string in MenuOption is just a buffer of 161 chars, which
is 40 chars (160 bytes if each were the largest possible UTF-8 character
size) plus a null terminator. This is because the maximum length of a
menu option that can be fit on the screen without going past is 40
chars.
There's no need to use a template here. Just manually call SDL_tolower()
or SDL_toupper() as needed.
Oh yeah, and use SDL_tolower() and SDL_toupper() instead of libc
tolower() and toupper().
I don't know how no one realized that the for-loop to (poorly)
initialize m_headers was basically unnecessary, and that the memset()
should've just been used instead. Well, except it should also be
replaced with SDL_memset(), but that's besides the point.
Also, I decided to hardcode the 128 thing less, in case people want to
fork the source code and make a build where it's changed.
So, originally, I wanted to keep them on Game, but it turns out that if
I initialize it in Game.cpp, the compiler will complain that other files
won't know what's actually inside the array. To do that, I'd have to
initialize it in Game.h. But I don't want to initialize it in Game.h
because that'd mean recompiling a lot of unnecessary files whenever
someone gets added to the credits.
So, I moved all the patrons, superpatrons, and GitHub contributors to a
new file, Credits.h, which only contains the list (and the credits max
position calculation). That way, whenever someone gets added, only the
minimal amount of files need to be recompiled.
They're always the same size, so there's no need for them to be vectors.
Also made the number of elements in ed.level/kludgewarpdir controllable
by maxwidth/maxheight.
I removed editorclass::saveconvertor() because I didn't want to convert
it to treat ed.contents like an array, because it's unused so I'd have
no way of testing it, plus it's also unused so it doesn't matter. Might
as well get rid of it.
These unused vars are:
- Graphics::bfontmask_rect
- Graphics::backgrounds
- Graphics::bfontmask
- GraphicsResources::im_bfontmask
While it seems that Graphics::backgrounds was indexed in
Graphics::drawbackground(), in reality there was never anything in that
vector and thus actually using it would cause a segfault.
map.contents always has 1200 tiles in it, there's no reason it should be
a vector.
This is a big commit because it requires changing all the level classes
to return a pointer to an array instead of returning a vector. Which
took a while for me to figure out, but eventually I did it. I tested to
make sure and there's no problems.
They're always fixed-size anyways, there's no need for them to be
vectors.
Also used the new INBOUNDS_ARR() macro for the map.explored bounds
checks in Script.cpp, and made map.explored a proper bool array instead
of an int array.
Again, basically no reason for it to exist on the class itself.
The usage of the variable was replaced with temp2 instead of temp
because there was already a temp variable in the function it was used
in.
Since they're always fixed-size, they don't need to be dynamically-sized
vectors.
entityclass::customcrewmoods is now a proper bool instead of an int now,
and I replaced the hardcoded constant 6 with a static const int Game
attribute to make it easier to change.
These are the besttimes, besttrinkets, bestlives, and bestrank
attributes of Game. bestframes was already a plain array.
As these are always fixed-sized, there's no reason for them to be
vectors. Also, I put their size in a static const int so it's easy to
change how many of them there are.
They're always fixed-size, so there's no need to them to be a dynamic
vector.
I changed their type to `bool` too because they don't need to be `int`s.
Also, I replaced the hardcoded 25 constant with at least a name, in case
people want to change it in the future.
It could index the `words` array out-of-bounds if there were more than
40 arguments in a command. Not like that would ever happen, but it's
still good to be sure.
In summary, if you got to gamestate 1002 or 1012 without an advancetext,
and you had completestop on, you were basically softlocked. So just add
those gamestates there and advance the gamestate if advancetext is off.
This infinite loop would occur because once you pressed left or right,
the game keeps searching through all the list of teleporters until it
finds one that is unlocked. But if there's none that are unlocked, then
the game goes into an infinite loop, which brings up the Not Responding
dialog on Windows so you can kill it.
Normally, you're not supposed to have no teleporters unlocked while
being able to access a teleporter, but you can achieve this by going to
Class Dismissed from a custom level (while making sure you don't start
in 0,0, because there's a teleporter there that you would unlock).
The solution is to make sure at least one teleporter is unlocked before
doing any searching.
A do-while is just a while-loop, but the inner block will always run
once before the conditional is checked.
It looks like in order to achieve this desired behavior (always run the
block once before checking the conditional), instead of using a do-while
loop, Terry just used a normal while-loop and copy-pasted the inner
block on the outside.
So I'm de-duplicating the code.
This was caused by the fact that not all unlocks were done through the
Game::unlocknum() function. Some just set the unlock number directly.
But it's fixed now.
Ved has this useful feature where instead of having to manually travel
to a room whose coordinates you know, you can just press G and type in
coordinates to go there.
VCE added this, but I changed the text to be "x,y" instead of "(x,y)"
because otherwise it could confuse someone into thinking they need to
type parentheses when in reality they don't need to and typing them will
just make it not work.
Also I made sure to add an error message if the user types in an invalid
format. Failing silently would just confuse people, and maybe they'll
start thinking the feature doesn't work or something like that. VCE
doesn't have this helpful error message.
Lastly, VCE has a bug where if you use the shortcut to go from one
horizontally/vertically warping room to another, the background of the
previous room will still be there and scroll off with the background of
the room you went to, instead of just having the new background only.
This is because they forgot a 'graphics.backgrounddrawn = false;'. But
don't worry, *I* didn't forget about it.
This is basically FIQ's patch from VCE, except he never upstreamed it
because he said something along the lines of it seeming to not fit the
purpose of upstream.
But anyway, it basically just de-duplicates all the text input and text
finishing handling code and cuts down on the large amount of copy-paste
in the editor functions. It makes things way more maintainable.
Interesting note, it seems like FIQ had the intent to refactor the text
input in editor settings (i.e. the level metadata details), but never
got around to it in VCE. Maybe we'll finish that job for him later.
The problem we're running into is entirely contained in the Screen - we need to
either decouple graphics context init from Screen::init or we need to take out
the screenbuffer interaction from loadstats (which I'm more in favor of since we
can just pull the config values and pass them to Screen::init later).
Allowing users to reverse cycle tilesets/tilecols/enemies prevents them
from having to press the hotkey a zillion times in order to get to the
one they want if the one they want just happens to be behind the current
one they're on.
This tilecol conveniently lets players use one of the unpatterned Space
Station tilesets you see on the left side of tiles.png but never get to
use without Direct Mode.
It does have a few weird quirks, but it should be safe to use.
Previously, it was:
if (ed.settingsmod)
{
(Settings menu controls)
...
}
else
{
(Literally everything else
Also a bunch of copy-pasted ed.keydelay checks)
...
}
Now it is:
if (ed.settingsmod)
{
(Settings menu controls)
...
}
else if (ed.keydelay > 0)
{
ed.keydelay--;
}
else if (key.keymap[SDLK_LCTRL] || key.keymap[SDLK_RCTRL])
{
// Ctrl modifiers
...
}
else if (key.keymap[SDLK_LSHIFT] || key.keymap[SDLK_RSHIFT])
{
// Shift modifiers
...
}
else
{
// No modifiers
ed.shiftkey = false;
...
}
It might not counteract how completely huge this code is, but it's at
least organized better.
Also, I had to change the map resize logic around slightly, else it'll
get triggered any time you do a shift modifier keypress.
Their drawframe needs to be incremented by 2 instead of 1, because
they're double-sized.
Animation type 3 is used by the cloud emitter in The Solution is
Dilution, animation type 6 is used by the radar dish in Comms Relay.
Animation type 4 is used by the maverick bus in B-B-B-Busted, but it's
not noticeable since it spawns offscreen. This bug would cause all of
those entities to appear incorrectly for the deltaframes between the
tick the room got loaded and the next tick after that.
This is noticeable in flibit's tweet showing off my over-30-FPS patch:
https://twitter.com/flibitijibibo/status/1273983014930993153
Now that you have a mini menu in MAPMODE, it's a bit annoying to have to
deal with the slowed-down timestep when pressing left/right/ACTION
inside it. Especially since going to an options menu restores the
timestep back to normal (because it's in TITLEMODE). Also removed it
from TELEPORTERMODE for consistency.
That way, they don't show up as "?: something else" and their proper
names are shown.
I didn't update the song numbers to include the newly-allowed songs
because otherwise it'd no longer correlate with what song numbers you
use for the music() simplified command.
This is basically just bolting on the "frames" part of a time trial
score. There's not enough space to properly show it on the time trial
select screen, maybe we can figure something out later. But I at least
want to implement the functionality now.
This doesn't make the editor completely accessible on controller, but
it's a good start at least. VCE already let you move between rooms with
the D-Pad, though.
These functions will only complain once if they receive an out-of-bounds
tile. And it's only once because these functions are called frequently
in rendering code.
A macro WHINE_ONCE() has been added in order to not duplicate code.
Disabling the one-way recolor if assets are mounted is needed to make
existing levels not look bad, but what about levels that want to use the
recolor anyway?
The best solution here is to just introduce another bool into the XML,
and make the re-color opt-in and only present if assets are mounted if
that tag is present.
Some levels (like Unshackled) have decided to manually re-color the
one-way tiles on their own, and us overriding their re-color is not
something they would want. This does mean custom levels with custom
assets don't get to take advantage of the re-color, but it's the exact
same behavior as before, so it shouldn't really matter that much.
I would've liked to specifically detect if a custom tiles.png or
tiles2.png was in play, rather than simply disabling it if any asset was
mounted, but it seems that detecting if a specific file was mounted from
a specific zip isn't really PHYSFS's strong suit.
One-ways have always had this problem where they're always yellow. That
means unless you specifically use yellow, it'll never match the tileset.
The best way to fix this without requiring new graphics or changing
existing ones is to simply re-tint the one-way with the given color of
the room. That way, the black part of the tile is still black, but the
yellow is now some other color.
Ved has this useful feature where you can "lock" a gravity line or warp
line in place, meaning it'll no longer extend its length until it
touches a tile. A line is locked if the p4 of the edentity is 1.
VVVVVV doesn't support this, but now it does. The horrifying thing is
that it stretches the lines out *while rendering the line*, so it looks
like logic and rendering aren't that separate after all (although, I
already learned that when I did my over-30-FPS patch).
This change was half-backported from the localization branch, except I
just came up with "scaling mode" as a better term than the more generic
"graphics mode". It doesn't make sense to still have the option be
called "toggle letterbox" because a third option (integer mode) was
added at some point.
The options for fullscreen and scaling mode were at the top, then there
were various other graphical options, and then the option to resize to
the nearest window size that is of an integer multiple was all the way
below that. Now that last option is moved to be right below the other
options related to window sizing.
VVVVVV's menus are kind of packed to the brim, so I thought it was time
to recategorize the menus a little bit. There's now a new "advanced
options" menu which holds the following options which were moved out of
graphic options, game options and especially accessibility options:
- toggle mouse
- unfocus pause
- fake load screen
- room name background
- glitchrunner mode
I also made the positioning of the titles and descriptions more
consistent, and made some options which were moved to the new menu not
so abbreviated ("load screen" and "room name bg")
For whatever reason, not all arguments of createentity() are exposed in
the command.
We have to keep in mind that (1) unspecified arguments default to 0
(instead of the 320 and 240 for argument 8 and 9 that createentity()
usually defaults to), and that (2) arguments persist across commands.
(Why not get rid of argument persistence, you say? Unfortunately, some
levels rely on argument persistence to call gotoposition() without
specifying the third argument, even though you're supposed to specify
all three arguments.)
To add these arguments without breaking levels, I re-added the
createentity() defaults of 320 and 240 for args 8 and 9, and then I
reset the new arguments afterwards when I'm done. Technically this could
be bad if other commands used those higher arguments, but none of them
really do. (Except createcrewman(), but it only sets argument 6 to 0
sometimes anyway, but argument 6 is already supposed to default to 0.)
The game for some reason had this thing where it would not draw the
diagonal background tiles if you had animated backgrounds turned off.
Which is weird, because spikes with that background are still drawn as
spikes with that background. And also, it doesn't do this for any of the
tower hallway rooms, which is inconsistent.
Better to simplify the logic in Render.cpp anyways by removing
graphics.drawtower_nobackground() and making it really clear what
exactly we'll do if backgrounds are turned off. ("Aren't we already not
drawing the background? What's this _nobackground() function for?")
Flip Mode will now be in the game options menu if either:
(1) You're playing the M&P version.
(2) You have it unlocked and you came here from the in-game pause
screen.
This is because if you're playing M&P, you'd have to close the game,
edit unlock.vvv, and re-launch the game to toggle Flip Mode, since
there's no other way to do so. And if you're playing the full version,
you'd have to save and exit your session in order to toggle Flip Mode.
If you want your game window to simply be exactly 320x240, or 640x480,
or 960x720 etc. then it's really annoying that there's no easy way to do
this (to clarify, this is different from integer mode, which controls
the size of the game INSIDE the window). The easiest way would be having
to close the game, go into unlock.vvv, and edit the window size
manually. VCE has a 1x/2x/3x/4x graphics option to solve this, although
it does not account for actual monitor size (those 1x/2x/3x/4x modes are
all you get, whether or not you have a monitor too small for some of
them or too big for any of them to be what you want).
I discussed this with flibit, and he said that VCE's approach (if it
accounted for monitor size) wouldn't work on high-retina displays or
high DPIs, because getting the actual multiplier to account for those
monitors is kind of a pain. So the next best thing would be to add an
option that resizes to the nearest perfect multiple of 320x240. That way
you could simply resize the window and let the game correct any
imperfect dimensions automatically.
The Alt+Enter Glitch is a funny glitch where if you press any toggle
fullscreen keybind during a cutscene, Viridian will stop moving (if
they're being moved by a walk()) and ACTION will start being held down
for them. Meaning in most cases you can interrupt a walk and flip at the
same time.
This can obviously break cutscenes if a casual just wants to toggle
fullscreen, so I'm fixing it. But it will be unfixed in glitchrunner
mode in case you want to glitch out custom levels (I know I do).
More information on the Alt+Enter Glitch is available here:
https://gitgud.io/infoteddy/vvvvvv-knowledge/blob/master/bugs/bugs.md#the-altenter-glitch
(The page is a bit outdated, some bugs have been fixed in 2.3 already.)
It's sometimes unwanted by people, and it's unwanted enough that there
exist instructions to hexedit the binary to remove it (
https://distractionware.com/forum/index.php?topic=3247.0 ).
Fun fact, the unfocus pause didn't exist in 2.0.
There were a few problems with the old way of doing things:
(1) Level stats were an ad-hoc object. Basically, it's an object whose
attributes are stored in separate arrays, instead of being an actual
object with its attributes stored in one array.
(2) Level filenames with pipes in them could cause trouble. This is
because the filename attribute array was stored in the XML by being
separated by pipes.
(3) There was an arbitrary limit of only having 200 level stats, for
whatever reason.
To remedy this issue, I've made a new struct named CustomLevelStat that
is a proper object. The separate attribute arrays have been replaced
with a proper vector, which also doesn't have a size limit.
For compatibility with versions 2.2 and below, I've kept being able to
read the old format. This only happens if the new format doesn't exist.
However, I also WRITE the old format as well, in case you want to go
back to version 2.2 or below for whatever reason. It's slightly
wasteful to have both, but that way there's no risk of breaking
compatibility.
Centiseconds won't be saved to any save file or anything. This is just
to make speedrunning a bit more competitive, being able to know the
precise time of a time trial or full game run.
The time trial par time on the result screen always has ".99" after it.
This is basically due to the game comparing the number of seconds to the
par number of seconds using less-than-or-equal-to instead of simply
less-than.
The only reason why gray Warp Zone entities were green originally was
because there is a giant concatenated list of tileset+tilecol
combinations, and by using tileset 3 tilecol 6 you're using the entry
of tileset 4 tilecol 0, which is the green Ship tileset.
So without interfering with the green Ship tileset's entry, I've decided
that the best thing to do is to just add special cases. The enemy color
was easy enough to fix. The platform color was also easy to fix.
However, there exist no existing textures for gray conveyors, so at that
point I decided to just tint the existing green one gray, and then I did
the same for platforms.
This patch is very kludge-y, but at least it fixes a semi-noticeable
visual issue in custom levels that use internal scripts to spawn
entities when loading a room.
Basically, the problem here is that when the game checks for script
boxes and sets newscript, newscript has already been processed for that
frame, and when the game does load a script, script.run() has already
been processed for that frame.
That issue can be fixed, but it turns out that due to my over-30-FPS
game loop changes, there's now ANOTHER visible frame of delay between
room load and entity creation, because the render function gets called
in between the script being loaded at the end of gamelogic() and the
script actually getting run.
So... I have to temporary move script.run() to the end of gamelogic()
(in map.twoframedelayfix()), and make sure it doesn't get run next
frame, because double-evaluations are bad. To do that, I have to
introduce the kludge variable script.dontrunnextframe, which does
exactly as it says.
And with all that work, the two-frame (now three-frame) delay is fixed.
It's annoying for casuals to have to close the game if they manage to
get themselves to turn into VVVVVV-Man, but it's amusing enough to
glitchrunners that they mess about with VVVVVV-Man in the main game,
clipping through walls everywhere (well, they call it Big Viridian, but
still).
Ironically enough, resetting more variables in script.hardreset() makes
the glitchy fadeout system even more glitchy. Resetting map.towermode,
for example, makes it so that if you're in towers when you quit to the
menu, script.hardreset() makes it so that the game thinks you're no
longer inbounds (because it no longer thinks you're in a tower and thus
considers coordinates in the space of 40x30 tiles to be inbounds instead
of 40x700 or 40x100 tiles to be inbounds), calls map.gotoroom(), which
resets the gamestate to 0. So if we're using the old system, it's better
to reset only as much as needed.
And furthermore, we shouldn't be relying on script.hardreset() to
initialize variables for us. That should be done at the class
constructor level. So I've gone ahead and initialized the variables in
class constructors, too.
This was fixed in 2.3 because one of the side effects of this janky
system was being able to accidentally immediately quit to the title if
the screen was black during a cutscene, which is something very likely
to happen to casual players.
Anyway, credits warp uses this gamestate-based system because it
utilizes quitting to the title screen doing gamestate 80. From there,
you increment the gamestate to gamestate 94 to use the Space Station 2
expo script.
This is the second part of what is necessary for credits warp to work.
The speedrunners call this "text storage". You need to get the
advancetext prompt up without a text box in order to be able to
increment the gamestate without bound. In 2.0, script.hardreset() reset
the text boxes, but not the prompt.
This is the first part of what is necessary for credits warp to work.
If the "- Press ACTION to advance text -" prompt is up, and you manage
to keep it up, then you can indefinitely increment the gamestate by
pressing ACTION.
This is first used in credits warp to teleport to the start of Space
Station 2 (by utilizing the Eurogame expo script, triggered by a
gamestate), and then again later by using a teleporter that has a high
gamestate number to increment to the [C[C[C[C[Captain!] cutscene.
Glitchrunner mode is intended to re-enable glitches that existed in
older versions of VVVVVV. These glitches were removed because they could
legitimately affect a casual player's experience. Glitches like various
R-pressing screwery, Space Station 1 skip, telejumping, Gravitron
out-of-bounds, etc. will not be patched.
It should've checked the final spacing and not the intermediate maximum
value. I had changed some things around, and now the minimum spacing
was 5 instead of 0 by mistake.
All menus had a hardcoded X position (offset to an arbitrary starting
point of 110) and a hardcoded horizontal spacing for the "staircasing"
(mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or
something else). Not all menus were centered, and seem to have been
manually made narrower (with lower horizontal spacing) whenever text
ran offscreen during development.
This system may already be hard to work with in an English-only menu
system, since you may need to adjust horizontal spacing or positioning
when adding an option. The main reason I made this change is that it's
even less optimal when menu options have to be translated, since
maximum string lengths are hard to determine, and it's easy to have
menu options running offscreen, especially when not all menus are
checked for all languages and when options could be added in the middle
of a menu after translations of that menu are already checked.
Now, menus are automatically centered based on their options, and they
are automatically made narrower if they won't fit with the default
horizontal spacing of 30 pixels (with some padding). The game.menuxoff
variable for the menu X position is now also offset to 0 instead of 110
The _default_ horizontal spacing can be changed on a per-menu basis,
and most menus (not all) which already had a narrower spacing set,
retain that as a maximum spacing, simply because they looked odd with
30 pixels of spacing (especially the main menu). They will be made even
narrower automatically if needed. In the most extreme case, the spacing
can go down to 0 and options will be displayed right below each other.
This isn't in the usual style of the game, but at least we did the best
we could to prevent options running offscreen.
The only exception to automatic menu centering and narrowing is the
list of player levels, because it's a special case and existing
behavior would be better than automatic centering there.
The tilesheets in question are font.png, tiles.png, tiles2.png,
tiles3.png, entcolours.png, teleporter.png, sprites.png, and
flipsprites.png.
This patch removes the hardcoded dimensions when scanning the
tilesheets, because it's simpler that way. It also de-duplicates it so
it isn't a bunch of copy-paste, by using macros. (I had to use macros
because it was the easiest way to optionally pass in some extra code in
the innermost for-loop.)
Also, if the dimensions of a scanned tilesheet aren't exactly multiples
of the dimensions of the tile unit for that given tilesheet (e.g. if the
dimensions of a scanned tiles.png are not exact multiples of 8), then an
SDL_SimpleMessageBox will show up with the error message, a puts() of
the error message will be called, and the program will exit.
It seems like they were unfinished. This commit makes them properly
work.
When a track is stopped with stopmusic() or musicfadeout(),
resumemusic() will resume from where the track stopped. musicfadein()
does the same but does it with a gradual fade instead of suddenly
playing it at full volume.
I changed several interfaces around for this. First, setting currentsong
to -1 when music is stopped is handled in the hook callback that gets
called by SDL_mixer whenever the music stops. Otherwise, it'd be
problematic if currentsong was set to -1 when the song starts fading out
instead of when the song actually ends.
Also, music.play() has a few optional arguments now, to reduce the
copying-and-pasting of music code.
Lastly, we have to roll our own tracker of music length by using
SDL_GetPerformanceCounter(), because there's no way to get the music
position if a song fades out. (We could implicitly keep the music
position if we abruptly stopped the song using Mix_PauseMusic(), and
resume it using Mix_ResumeMusic(), but ignoring the fact that those two
functions are also used on the unfocus-pause (which, as it turns out, is
basically a non-issue because the unfocus-pause can use some other
functions), there's no equivalent for fading out, i.e. there's no
"fade out and pause when it fully fades out" function in SDL_mixer.) And
then we have to account for the unfocus-pause in our manual tracker.
Other than that, these commands are now fully functional.
This fixes a corner case where using gamestate 82 from the editor would
put you in a softlock because it would return to the editor settings
menu, which only functions in EDITORMODE and results in a softlock in
TITLEMODE.
This is already done for invincibility. It's kind of unnecessary, but
it's just to make sure if for some reason in the future variables like
insecretlab/intimetrial/nodeathmode don't get reset when exiting to the
menu.
To fix this annoying flicker (which, btw, took me WAY too long to do), I
had to introduce yet another kludge variable to signal that the
horizontal/vertical warp background should be re-initialized on the
pause screen.
I think I could technically keep the 'graphics.backgrounddrawn = false;'
in maplogic() and remove the 'graphics.backgrounddrawn = false;' in
Game::returntopausemenu(), but I'm keeping that other one around because
it doesn't hurt and just as a general precaution and safety measure.
This makes it so the tower background doesn't persist and scroll upwards
if you exit the menu in a warp zone horizontal or vertical room.
Ugh, and while we're on the subject of separating the in-game tower
background and the menu tower background, could we PLEASE separate the
horizontal / vertical warp backgrounds from the tower backgrounds, too?!
Another thing that's annoyed me a lot is being unable to simply press
Esc to close the pause menu. You'd have to hover over the "return to
game" or "keep playing" option. This would be even more annoying with
more options on the menu, so allowing to press Esc is a nice
quality-of-life thing.
Otherwise you could keep re-pressing ACTION on the "yes" option and keep
stalling it until it finally faded out, or quickly go back past menu
options or something.
Otherwise the menu background would have this rendering glitch where the
bypos of the in-game tower wouldn't divide easily and have a bunch of
jitters in an otherwise smooth but overall still somewhat smooth
background.
Also set map.tdrawback to true when leaving the menu.
This is to fix the interpolated color of the tower background
persisting, as well as making sure the menu background doesn't persist
when exiting.
This would be fine, under the assumption that you could never reach the
menu from outside the menu. Well, now you can, so now this has to play
the correct song instead of track 6.
This is to pre-emptively prevent piling up stack frames for what I'll be
adding next, which is pressing Esc in the options menu in-game
automatically moving you back to MAPMODE.
Since the exact same tower background is also used on the menu, we need
to save the current state of the background when entering the menu
(before overwriting it), and then put it back when we're done. Maybe we
ought to separate the in-game and menu tower backgrounds...
This also fixes a semi-hilarious bug where you could make Panic Room go
in the other direction by simply going to the options menu in-game.
This is accomplished by adding convenience functions
mapclass::bg_to_kludge() and mapclass::kludge_to_bg().
Well this is a bit annoying. I can call graphics.updatetowerbackground()
just fine, but I have to get at the title color update routine inside
titlelogic(), which is hard-baked in. So I have to pull that code
outside of the function, export it in the header, and then call it when
I transition to TITLEMODE.
So now the options do what they do. However, I still need to fix the
1-frame glitch when switching to TITLEMODE, as well as make returning
from the menu return back to MAPMODE, as well as making this better menu
integrate seamlessly with the existing menus.
This prevents from having to repeat 'if (game.menupage == ...)'
everywhere, which makes for more concise code.
I know you're technically supposed to indent the cases surrounded by
if-guards, but I don't think indenting them here would help anything.
I'd only indent it if the 'if' had an 'else', for example. But if it
surrounds the whole case, then there's no need for indentation.
Similar to Graphics::map_tab(), this ensures that I don't have to
copy-paste printing the map options for every single game.menupage case
I want, and in this case that's a good thing because there'll be 4
game.menupage cases I'll be using.
This basically adds an extra '|| game.inintermission' because it seems
like the original code forgot about that conditional. You can't save in
level replays, so there's no need to say "You will lose any unsaved
progress." in intermission replays.
In my previous PR, I wrongly assumed that I could just replace the `i`
handling code of those options with an `i++;` at the beginning, and thus
I could put all blocks' `i++;` into ARG_INNER(). Well I was wrong,
because the code was written the original way for a reason, namely that
we still need `i` to point to the -playx/y/rx/ry/gc/music argv so we can
re-compare which argument led us into this code block.
Any decent compiler will optimize this so that it's still only two
function calls (or it gets inlined). However, it's still not very
readable, so I've assigned the result to a variable and used that
instead.
Instead of copy-pasting everything and changing a few parts, just have
something that handles that tiny part. This reduces the amount of code
size the custom level CREW page takes up by half.
This will be used to keep some text positions the same when in Flip
Mode, instead of having to copy and paste code.
This function being at the very top of the file kind of violates
locality, but it has to be done because it can't be a macro.
Previously, the code to print all tab names was copied to every single
tab, resulting in 12 more superfluous print statements than needed. This
commit uses graphics.map_tab to de-duplicate all the code.
This function is useful to de-duplicate all the map page names at the
bottom, which are MAP, CREW/SHIP/GRAV, STATS, and SAVE. If selected, it
will surround the text in square brackets and automatically handle the
positioning.
Shamelessly copy-pasted from Dav999's localization branch.
Just to be helpful if someone has a failing FILESYSTEM_init(), but
doesn't know that's their issue and keeps wondering why VVVVVV just
exits with code 1.
The command-line argument parsing code has a lot of copy-paste. This
copy-paste would get even worse if I added safety checks to make sure
you couldn't index argv out-of-bounds by having an argument like
`-renderer` without having anything after it, i.e. you'd be doing the
command `./VVVVVV -renderer`.
Previously, only the playtest arguments (apart from the recently-added
`playassets`) had this safety check, but the message it printed whenever
the safety check failed was always "-playing option requires one
argument" regardless of whatever argument actually failed to be parsed.
So I fixed it so that all arguments actually output the correct
corresponding failed argument instead.
Also, `strcmp(argv[i], <name>) == 0` is really kind of noisy, even if
you understand what it does perfectly well.
So I refactored it with a bunch of macros. ARG() just does the strcmp()
char* comparison, and ARG_INNER() does the safety check and returns 1,
along with printing a message, if the safety check fails.
This is used if you're loading a level file from STDIN. The game needs
to know the actual level assets directory you're referring to, since
when it gets the level from STDIN, it doesn't know the actual filename
of the level.
Fixes#309.
The assets mounting code was put directly in editorclass::load(), but
now it's in a neat little function so it can be called from multiple
places without having to call editorclass::load().
This ensures that endsWith() can be used outside of editor.cpp.
When leo60228 originally wrote endsWith(), it was static, but I asked
him on Discord just now and he more-or-less confirmed that it's fine if
it's not static. If it was static, it would be confined to
UtilityClass.cpp now instead!
Ugh, this is terrible and stupid and I hate myself for it.
Anyway, since the SDL2 VSync hint only applies when the renderer is
created, we have to re-create the renderer whenever VSync is toggled.
However, this also means we need to re-create m_screenTexture as well,
AND call ResizeScreen() after that or else the letterbox/integer modes
won't be applied.
Unfortunately, this means that in main(), gameScreen.init() will create
a renderer only to be destroyed later by graphics.processVsync().
There's not much we can do about this. Fixing this would require putting
graphics.processVsync() before gameScreen.init(). However, in order to
know whether the user has VSync set, we would have to call
game.loadstats() first, but wait, we can't, because game.loadstats()
mutates gameScreen! Gahhhhhh!!!!
@leo60228 suggested to fix that problem (
https://github.com/TerryCavanagh/VVVVVV/pull/220#issuecomment-624217939
) by adding NULL checks to game.loadstats() and then calling it twice,
but then you're trading wastefully creating a renderer only to be
destroyed, for wastefully opening and parsing unlock.vvv twice instead
of once. In either case, you're doing something twice and wasting work.
Otherwise a NO_CUSTOM_LEVELS build would fail. Also I got rid of the
'graphics.flipmode = false;' in the fixed loop because I don't think it
does anything.
This is needed for the next step. I want to put all the loop stuff in
their own functions so the code isn't one huge blob, but to do that I'll
need to make 'time' a global variable, but I can't do that because
actually 'time' is already a function, apparently, and you're only
allowed to shadow variables when already inside a function.
Like actual entities, editor ghost colors were updating every render
frame instead of logic frame. So just like actual entities, I added a
realcol attribute to them that editorrender() uses instead, and added
code to update said realcol attribute in editorlogic(). That way the
colors don't go by too quickly (especially the death color).
Just like all the other fixes, the variable that controls the amount of
ghosts to show was being updated every render frame instead of every
logic frame.
This is because due to the game loop changes in this over-30-FPS patch,
editorrender() can be called and undo graphics.backgrounddrawn being set
to false once again. Solution here is to make it so it keeps being set
to false until game.shouldreturntoeditor is turned off, which has also
been moved to editorlogic().
This is to make sure no lerping occurs in 30-FPS mode, otherwise things
might look weird. A good case that this fixes is how entities look in a
double-gotoroom (they should be completely frozen in 30-FPS mode).
There is now an option in "graphic options" named "toggle fps", which
toggles whether the game visually runs at 1000/34 FPS or over 1000/34
FPS. It is off by default.
I've had to put the entire game loop in yet another set of braces, I'll
indent it next commit.
What happens here is that the entity gets created and then gets
immediately updated on the next frame, but there's no time for their
walkingframe of 0 to be rendered, so it'll look like they have just
started with walkingframe 1. However in the delta-timestep rendering
it'll render with walkingframe 0. So we need to fix their drawframe and
increment it when creating them.
Looks like mapclass::changefinalcol() is called whenever you enter a
room in Outside Dimension VVVVVV.
mapclass::changefinalcol() changes the tile, but it doesn't update their
drawframe. So after that function is called, update their drawframe.
If you update their drawframe while inside that function, then when the
platform actually cycles color it'll cycle backwards quickly sometimes,
which is not ideal.
When I loaded the room where Vitellary is in Space Station 2, I saw this
1-frame glitch happen despite my previous efforts to prevent it. So now
it's fixed.
This is because otherwise, on my Linux system at least, the game will
take up a lot of CPU that it doesn't really need to (I only have a 60hz
monitor).
On Windows, it looks like Windows already enforces V-sync for
applications anyway, unless they have exclusive fullscreen control.
Linux doesn't enforce V-sync on apps and lets them take up as much CPU
as they want, so I'm putting this here to limit the framerate.
The game is already actually 30 FPS anyway, the nice smooth FPS is just
visual. V-sync won't introduce any more "input lag" than already exists.
By "hidden names", I'm referring to "Dimension VVVVVV" and "The Ship"
popping up on the quit/pause/teleporter screens, even though those rooms
don't have any roomnames.
Apparently my commit to fix roomname re-draw bleed on the
quit/pause/teleporter screens exposed yet another hardreset()-caused
bug. The issue here is that since hardreset() sets game.roomx and
game.roomy to 0, map.area() will no longer work properly, and since the
hidden roomname check is based on map.area(), it will no longer display
"Dimension VVVVVV" or "The Ship" once you press ACTION to quit. It used
to do this due to the re-draw bleed, but now it doesn't.
I saw that roomnames didn't get reset in hardreset(), so the solution
here is to re-factor hidden names to be an actual variable, instead of
being implicit. map.hiddenname is a variable that's set in
mapclass::loadlevel(), and if isn't empty, it will be drawn on the
quit/pause/teleporter screens. That way it will still display "Dimension
VVVVVV" and "The Ship" when you press ACTION to quit to the menu.
EDIT: Since PR #230 got merged, this commit is no longer strictly
necessary, but it's still good to refactor hidden names like this.
As much as it looks cool to have a slowly-scrolling background on the
title screen, it's quite annoying that slowmode applying on the title
screen mean that your keypresses are less responsive.
This adds Graphics::crewcolourreal(), which is like the
entityclass::crewcolour() that the editor already uses, except for the
real color instead of the color ID. Also, editorclass now has an
attribute `entcolreal` so enemy colors don't update more than 30 frames
a second.
The solution is to draw another row of incoming textures. And also just
draw another row of textures when the background needs to be redrawn,
otherwise it'll flicker when the color changes while you're holding down
ACTION.
To fix this, I draw another row/column of incoming textures. But of
course, I have to extend the size of the towerbuffer, otherwise the
incoming textures will just be gone.
This could happen if you held down ACTION in the credits, looks like the
background doesn't keep up for some reason. That's another bug to fix,
but at least I can fix this overdraw.
This is only noticeable if you have a font with translucent pixels, like
I do. But it gets really noticeable really quickly with this over-30-FPS
patch because the render functions are continuously called every
delta-timestep. To prevent this, just fill the backbuffer with black
before rendering anything.
There's still a problem in that the flickering that would lead to this
overdraw in the first place still exists. But at least if it'll flicker,
it'll flicker black and not overdraw.
Currently it interpolates it based on the current state of game.swngame,
but when game.swngame changes the interpolation doesn't know that it has
JUST changed or anything. So add a kludge variable to fix this
off-by-one.
This was especially noticeable in slowmode, where after going to an
adjacent room, it would look like they stopped for a split second. This
commit makes it so they smoothly continue their journey after switching
rooms.
The integer cast is to round off any fractional part of the velocity so
that they don't make a difference and result in the oldxp/oldyp being
one pixel off. Especially since the player's y-velocity fluctuates while
standing unflipped on the floor.
Incidentally enough, this seems to only have been a problem with screen
transitions for some reason. No other uses of gotoroom() (such as the
one where gotoroom() is called every other frame, or every frame) seem
to have resulted in this "pausing" behavior, or at least a reversion
back to 30 FPS movement. I don't know why.
Otherwise it'll go by too quickly.
Also something subtle here - I didn't make it conditional on
game.advancetext, so now it'll still decrement even if you have
advancetext up.
This makes editor notes fade out smoothly. And even though the notedelay
only gets decremented by one every editor-frame (the editor runs at
1000/24 FPS fixed-timestep here), it actually gets multiplied by 4, so a
floating-point interpolated value would make a difference here.
This is because their oldxp wasn't being updated when they move (or
rather, teleport) and wrap around the screen.
These enemies are ZZT centipedes, but they're referred to as ASCII
snakes in comments in the code.
These colors were of the colors of each crewmate, the inactive crewmate
color, and the color of the trinket and clock on the quicksave/summary
screens.
These colors all used fRandom() and so kept updating too quickly because
they would be recalculated every time the delta-timestep render function
got called, which isn't ideal. Thus, I've had to add attributes onto the
Graphics class to store these colors and make sure they're only
recalculated in logic functions instead.
Thankfully, the color used for the sprites on the time trial results
screen doesn't use fRandom(), so I don't have to worry about those.
There's a new version of Graphics::drawsprite() that takes in a pre-made
color already, instead of a color ID. As well, I've also added
Graphics::updatetitlecolours() to update these colors on the title
screen.
Okay, so the problem here is that Graphics::setcol() is called right
before a sprite is drawn in a render function, but render functions are
done in deltatime, meaning that the color of a sprite keeps being
recalculated every time. This only affects sprites that use fRandom()
(the other thing that can dynamically determine a color is help.glow,
but that's only updated in the fixed-timestep loop), but is especially
noticeable for sprites that flash wildly, like the teleporter, trinket,
and elephant.
To fix this, we need to make the color be recalculated only in the
fixed-timestep loop. However, this means that we MUST store the color of
the sprite SOMEWHERE for the delta-timesteps to render it, otherwise the
color calculation will just be lost or something.
So each entity now has a new attribute, `realcol`, which is the actual
raw color used to render the sprite in render functions. This is not to
be confused with their `colour` attribute, which is more akin to a color
"ID" of sorts, but which isn't an actual color.
At the end of gamelogic(), as well as when an entity is first created,
the `colour` is given to Graphics::setcol() and then `realcol` gets set
to the actual color. Then when it comes time to render the entity,
`realcol` gets used instead.
Gravitron squares are a somewhat tricky case where there's technically
TWO colors for it - one is the actual sprite itself and the other is the
indicator. However, usually the indicator and the square aren't both
onscreen at the same time, so we can simply switch the realcol between
the two as needed.
However, we can't use this system for the sprite colors used on the
title and map screen, so we'll have to do something else for those.
In order to make sure colors don't update more than 1000/34 frames per
second, I'll have to move the color-setting part of this function
somewhere else.
Just a small thing, but if you teleported out of a tower with the
top/bottom screen spikes being onscreen (by dying, for example), they
would retract once you went back in to a tower. Small little thing, but
it's a good thing to polish.
Otherwise, the tile animations will go too fast. However, the overall
color cycling hasn't been going fast, since it was never in gamerender()
in the first place.
When you pressed and released ACTION to speed up the credits, the
credits position would end up being 1 frame off from the background.
This is due to the fact that we update the tower background after we
update the credits position, so this commit moves the tower background
update before the credits position update.
These special images are the crewmates, Level Complete, and Game
Complete images. They flashed depending on if you were lucky and
happened to got your delta-timesteps just right when text boxes were
fading in and out.
Honestly, I'm surprised text box fading in/out hasn't ran into this
issue before. It's insane luck that this issue hasn't occurred before or
anything.
Well, anyways, to fix this, there's now an attribute `allowspecial` on
text boxes, and an optional parameter of the same name for
Graphics::createtextbox(). This attribute is the only thing that will
let these special text box images render. And any createtextbox()es that
utilize these special images have been updated accordingly.
By "frame" here I'm referring to the fixed-timestep, not a visual
delta-timestep.
Anyway, the problem is because crewmates' drawframes wait for
entityclass::animateentities() to be called in gamelogic(). In the
old, 30-FPS-only system, this entityclass::animateentities() would be
called in gamerender(), before any actual rendering would take place.
However, I've had to move it out of gamerender() because otherwise
entities would animate too fast. As a result, since gamerender() could
be called in between the entity creation and gamelogic(), a
less-than-1-frame visual glitch could happen.
The solution is to set the entity's drawframe as well when fixing facing
the player in Map.cpp.
Incidentally enough, this de-duplicates the amount of times this code
has been copy-pasted from 4 times to 2.
Anyways, this makes it so the crew don't go lightning fast on the
summary screen, the NDM game-over screen, the NDM win screen, and the
pause screen in the main game. It was slightly hilarious seeing them go
really quickly, actually. It was like they were running away from a
giant monster or something.
Just to make sure it's extra smooth. Not that it will be noticeable, and
you can't access the Secret Lab in slowmode without modifying the game,
but it's nice to have this.
Otherwise it'll go by really fast and rapidly pulsate. To the point
where it seems like it would be an epilepsy trigger, although I
wouldn't know anything about epilepsy other than that it's bad.
Ok, now THIS takes the cake for "only really noticeable in slowmode",
because it only ever moves at 1 pixel per second. And even then,
slowmode shouldn't apply on the title screen, so it won't even show up
there once I get around to doing that change.
This is so the background doesn't NYOOOOM past at light speed. Although
for a game set in space like VVVVVV, light speed ain't bad.
And this finally requires that editorlogic() have a call to
Graphics::updatebackground().
To make it real smooth, just in case it was noticeable that it only
updated at 1000/34 FPS before (well, except in slowmode, it's really
noticeable THERE).
Also this removes the re-typing out of (game.act_fade/10.0f) for every
single R, G, and B in gamerender().
This makes text boxes fade in and out pretty smoothly.
This requires that the textboxclass::setcol() be in Graphics::drawgui(),
so now it's moved there.
Text box fading is only really noticeable if you're playing in slowmode.
This has to be done in order to fix rendering when on a conveyor or
moving platform and actively moving with or against it. Pretty sure this
shouldn't break anything, oldxp/oldyp is mostly visual after all (and by
the time it's used for gravity line collision checking,
updateentitylogic() would've already gotten around to it anyway).
Incidentally, this also fixes a jitter that would occur if you were
moving at the time you died or collected a trinket or custom crewmate,
due to the game temporarily freezing and either doing deathsequence or
completestop.
Now it's really, really smooth. Except for like the last frame when it
goes down, which I sometimes didn't notice (but maybe it didn't happen
every time due to being lucky on the delta timesteps or something,
whatevs.)
Since "if (graphics.resumegamemode)" and "if (menuoffset > 0)" both do
the same thing, they've been combined with an "or" conjunction.
As well, the map.extrarow check in maplogic() has been refactored to use
a variable instead of duplicating the entire code block. Not that it
matters anyway, because the difference between 240 and 230 is only 10
pixels, far short of the 25 pixel increment that bringing the menu up
and down uses, and both 240 and 230 integer-divided by 25 have the same
non-remainder value of 9.
Otherwise the screen will shake too fast for my liking.
Also I'm planning to add an FPS limiting option later (because right
now, un-capping the FPS is pretty wasteful and eats up lots of
resources, especially since I have only a 60hz monitor), and it'd feel
weird if screen shaking updated every delta timestep.
This fixes entities being drawframe 0 for 1 frame when being first
created. Incidentally, this also fixes entities created during
completestop being the player sprite, too, which is something not many
people notice.
For some reason, it was put near the start of gamerender(), even though
since it handles edge-flipping it seems like it should be in the logic
function already.
This makes sure entity animations don't animate as fast as possible, and
also fixes edge-flipping on normal surfaces.
This prevents undefined behavior because we use oldxp/oldyp to do linear
interpolation.
It's also initialized in entclass::entclass(), just to be sure. And I've
deduplicated the regular xp/yp initialization in createentity(), too.
I've added a function Graphics::lerp() which simply interpolates between
two values given a certain alpha value. It's just like drawing a
straight line between two points.
Also, Graphics now has an `alpha` attribute, and it is set on every
deltatime update to be used in linear interpolation.
Ok, and this is where the fun starts.
In an ideal world, this would be the end of this patch. However, of
course, there are many, MANY places in the game that update
fixed-timestep timers DIRECTLY inside the render function, which is not
ideal because it means those timers go super fast.
I'll have to fix those later.
Ok, NOW indent it. I didn't indent it previously because the diffs are
annoying to read if there's an indent that doesn't otherwise change
anything (and even now it's pretty annoying to read).
Alright, this is the start of the over-30-FPS patch!
First things first, we'll need to make it possible to have a separate
deltatime loop outside of the fixed timestep loop. And for that, we
can't be using SDL_Delay(), as SDL_Delay() (as you might imagine) blocks
the whole program.
Instead we'll be using this thing called an accumulator. It looks at how
long the previous poll took (the raw deltatime), and lets timesteps pass
accordingly.
On a side note, I've had to split the `time` and `timePrev` declaration
each onto their own separate line, otherwise there's undefined behavior
from `time` not being initialized.
I use `accumulator = fmodf(...)` instead of `accumulator -=
timesteplimit` because otherwise it'll fast-forward if it's behind,
which is a jarring thing to see.
Also in preparation for what's going to come down the over-30-FPS road,
I've also added `deltatime` and `alpha`. `deltatime` is going to be used
if the game is in slowdown mode, and `alpha` is going to be used for
linear interpolation of animations.
By the way, what was the main game loop previously (and is now the new
timestep loop) is now in an extra set of curly braces, but I haven't
indented it yet to reduce the noise in this commit.
This prevents being able to "roll over" the amount of minutes to 0 (by
simply waiting for the timer to tick past one hour) and being able to
get a result of 00:13 when your result is really 01:00:13.
By looking only at the minutes, the game would read 01:00:13 as 00:13
instead. So simply add the amount of hours to the time trial result.
This is needed for MinGW when compiling C++98, apparently. I put it in
an if-guard because otherwise there'll be a warning from MY compiler
about redefinitions.
If you exited to the menu normally (i.e. got on a code path that went
through Game::quittomenu()), the menu music wouldn't play. This is
because FILESYSTEM_unmountassets() was put after music.play(6). So the
game would play the custom level's track 6, and then unmount it, which
meant it could no longer play track 6, but there's nothing telling the
game to play track 6 again. So I just changed the frame ordering around.
I also added a comment to make sure anyone reading the code is aware of
the frame order dependency.
If you exited from the editor, custom assets would not be unmounted. But
I made sure to put the FILESYSTEM_unmountassets() before the
music.play(6) because otherwise the menu music wouldn't play.
You could also exit to the menu from a custom level using the
rollcredits() command, so I made sure to put a
FILESYSTEM_unmountassets() when returning to the menu from the credits
as well. I also made sure to put it before the music.playef(18) so
there's no risk of the sound effect not playing properly, or not playing
the non-level-specific one.
I added a comment to both FILESYSTEM_unmountasset()s to make sure anyone
reading the code is aware of the frame order dependency.
This is really awful, but there's not much we can do.
TinyXML-2 no matter what will never stop on newlines, so without
changing the XML parser, this is the best we can do - just remove the
"\n " (that's a linefeed plus exactly 12 spaces) if it
appears at the end of the contents of an edentity tag.
Also a giant comment for good measure.
Looks like duplicate player entities persisting across rooms is a
semi-useful feature used by some levels. Still, though, it's a bit of a
nuisance to have duplicate player entities persisting across game
sessions. And levels can't rely on this persistence anyway, anyone could
just close the game and re-open it to get rid of the duplicate entities
regardless.
If a trinket or crewmate ID is out-of-bounds, it will not be created.
This is not only because the `collect`/`customcollect` check in
entityclass::createentity() would then be out-of-bounds, but also
touching it would also be out-of-bounds, too.
Display trinkets will always be created if the ID is out-of-bounds.
Apparently some people (@AllyTally) have been creating display trinkets
with IDs of -1 in order to get a display trinket that always shows up,
which is rather horrifying. But it makes sense, there's a lot more
nonzero int values than there are the amount of int values that are
zero, so it's fairly likely that the `collect` check will always come up
to be true (nonzero). Also, it's more useful to be able to have a
display trinket that always shows up without having to collect a trinket
beforehand, than it is to have it not be created (because technically by
default, you're already in the world where you don't have it created).
Display trinkets still have their `para` set to their ID, though, and if
they managed to gain an `onentity` of 1, bad things could happen... So
just to be sure, I added INBOUNDS checks to crewmates and trinkets in
entityclass::updateentities() so no UB will happen if you collect a
crewmate/trinket with an out-of-bounds ID. Also, I de-duplicated the
`collect`/`customcollect` setting, too.
The flashy color of the elephant can be hard on people's eyes,
especially if they're the type who want screen effects disabled because
they might have epilepsy. The elephant takes up a good 3/4ths of the
screen, you know. If screen effects are disabled, the elephant will use
color 22, which is a neutral gray.
I'm only adding this because the VVVVVV speedrun mods (@tzann, @mohoc)
invalidate all runs that have the elephant texture removed, even though
many people would be looking at a potentially epilepsy-inducing image
many times a day grinding 100% speedruns. (Imo, their justification for
this is flimsy at best.)
The only class that actually needs its i/j/k kept is scriptclass,
because some custom levels rely on it for creating custom activity
zones. So I haven't touched that.
Other than that, there's no chance that anything important relies on
i/j/k in any other class. For that to be the case, it would have to use
i/j/k without initializing it beforehand, and that can simply be
detected by removing the attribute from the header file and seeing where
the compiler complains. And the compiler complains only about cases
where it's initialized first. (Note that due to this check, I *haven't*
removed Graphics's `m` as it precisely does exactly this, using it
without initializing it first.)
Interestingly enough, otherlevelclass and towerclass have unused i/k
variables for whatever reason.
This prevents the game from being saved if you manage to trigger a
savetele() during a "special" gamemode (like if you use the Gravitron
out-of-bounds glitch when replaying Intermission 2, then go to Game
Complete that way).
If you don't have a font.txt, it could happen that a font index is
requested that's out-of-bounds. And that would result in a segfault. So
to fix that I'm adding INBOUNDS checks to all functions that index the
fontmap.
This fixes indexing out-of-bounds in the functions that draw all the
special images such as the elephant and teleporters. Let's make sure the
game doesn't segfault.
I tracked down all the functions that took in an entity's drawframe and
made sure that no matter what value an entity's drawframe was, the game
would never segfault.
To deal with using a different image file for Flip Mode, it looks like
copy-paste was used. This isn't exactly maintainable code. So I'm
replacing it with a reference that changes depending on if the game is
in Flip Mode or not, instead.
Removing the player entity has all sorts of nasty effects, such as
softlocking the game because many inputs require there to be a player
present, such as opening the quit menu.
The most infamous glitch to remove the player entity is the Gravitron
Fling, where the game doesn't see a gravity line at a specific
y-position in the current room, and when it moves the bottom gravity
line it moves the player instead. When the gravity line gets outside the
room, it gets destroyed, so if the player gets dragged outside the room,
they get destroyed, too. (Don't misinterpret this as saying anytime the
player gets dragged outside the room, they get destroyed - it's only the
Gravitron logic that destroys them.)
Also, there are many places in the code that use entity-getting
functions that have a fallback value of 0. If it was possible to remove
the player, then it's possible for this fallback value of 0 to index
obj.entities out-of-bounds, which is not good.
To fix this, entityclass::removeentity() is now a bool that signifies if
the entity was successfully removed or not. If the entity given is the
player (meaning it first checks if it's rule 0, just so in 99% of cases
it'll short-circuit and won't do the next check, which is if
entityclass::getplayer() says the indice to be removed is the player),
then it'll refuse to remove the entity, and return false.
This is a change in behavior where callers might expect
entityclass::removeentity() to always succeed, so I changed the
removeentity_iter() macro to only decrement if removing the entity
succeeded. I also changed entityclass::updateentities() from
'removeentity(i); return true;' to 'return removeentity(i);'.
Apparently it results in Undefined Behavior if the argument given isn't
representable as an unsigned char. This means that (potentially) plain
char and signed chars could be unsafe to use as well.
It's rare that this could happen in practice, though. std::isdigit() is
only used by is_positive_num() which is only used by find_tag(), so
someone would have to deliberately put something crazy after an `&#` in
a custom level file in order for this to happen. Still, better to be
safe than sorry and all.
This fixes a compile error that could happen where the compiler doesn't
know what std::isdigit() is, but I'm puzzled as to why this wasn't
happening earlier, 'cause I've only been reported that it happens by
only one person.
Continuing from #280, another potential source of out-of-bounds indexing
(and thus, Undefined Behavior badness) comes from script commands. A
majority of them don't do any input validation at all, which means the
potential for out-of-bounds indexing and segfaulting in custom levels.
So it's always good to add bounds checks to them.
Interesting note, the only existing command that has bounds checks is
the flag() command. That means you can't turn out-of-bounds flags on or
off. But there's no bounds checks for ifflag(), or customifflag(), which
means you CAN index out-of-bounds with those commands! That's a bit bad
to do, so.
Also, I decided to add the bounds checks for playef() at the
musicclass::playef() level, instead of just the level of the playef()
command. I don't know of any other cases outside of the command where
musicclass::playef() will index out of bounds, but musicclass is the one
containing the indexed vector anyway, I wanted to cover more cases, and
it's better to be safe than sorry.
And this the function with the least amount of cases where its sentinel
value is used unchecked. Thankfully. obj.getplayer() was a bit of a slug
to get through.
obj.getplayer() can return -1, which can cause out-of-bounds indexing of
obj.entities, which is really bad. This was by far the most changes, as
obj.getplayer() is the most used entity-getting function that returns
-1, as well as the most-used function whose sentinel value goes
unchecked.
To deal with the usage of obj.getplayer() in mapclass::warpto(), I just
added general bounds checks inside that function instead of changing all
the callers.
A few months ago, I added ghosts to the VVVVVV: Community Edition editor. I was told recently I should think
about upstreaming it, and with Terry saying go ahead I finally ported them into VVVVVV. There's one slight
difference however--you can choose whether you have them or not in the editor's settings menu. They're off by
default, and this is saved to the save file.
Anyway, when you're playtesting, the game saves the players position, color, room coordinates and sprite every 3
frames. The max is 100, where if it tries to add more, the oldest one gets removed.
When you exit playtesting, the saved positions appear one at a time, and you can use the Z key to speed it up.
[Here's a video of them in action.](https://o.lol-sa.me/4H21zCv.mp4)
2.2 and earlier had this god-awful thing where it put the closing tag of
an edentity onto the next line, and then kept the indentation the same.
This requires parsing the XML in an extremely specific way (i.e.
ignoring the whitespace) so the newline and indentation isn't taken as
part of the actual contents of the tag.
2.3 removed this awful whitespace entirely to make it easier on parsers.
When I tested #270, I tested against a 2.3 re-save of Dimension Open and
diffed the two, because I thought testing against the original version
of the level would result in a bunch of noise I didn't want due to the
whitespace change. Well, I did exactly what I intended, and ended up
ignoring the whitespace change so much that levels saved in this stupid
format ended up getting broken.
Luckily, we can just tell TinyXML-2 to parse a document exactly like how
TinyXML-1 would've parsed it, by supplying the COLLAPSE_WHITESPACE enum
to it (by default it's on PRESERVE_WHITESPACE).
This removes the TinyXML source files, removes it from CMakeLists.txt,
removes all the includes, and removes the functions
FILESYSTEM_saveTiXmlDocument() and FILESYSTEM_loadTiXmlDocument() (use
FILESYSTEM_saveTiXml2Document() and FILESYSTEM_loadTiXml2Document()
instead).
Additionally I've cleaned up the tinyxml2.h include in FileSystemUtils.h
so that it doesn't actually include tinyxml2.h unnecessarily, meaning a
change to TinyXML2 shouldn't rebuild all files that include
FileSystemUtils.h.
Seems a bit wasteful to do the whole "parse the XML document thing"
instead of a simple file check. It doesn't even fail if the XML document
is invalid, but whatever.
Ok, so it was a bit of a struggle at first figuring out the new API, but
honestly it wasn't so bad in the end.
I made a copy of my old unlock.vvv before testing this, and checking
with `diff` the only difference is the new `encoding="UTF-8"` in the XML
declaration, which isn't a bad thing.
Surprisingly, I only had to change some names and stuff around at the
top of the function. The rest of the function could be left untouched
and it worked fine.
Some of the file was indented with two spaces and the rest indented with
tabs. It feels like two different people worked on the file, one more
than the other. Since most of it uses two spaces, I'll just replace the
tabs with two spaces.
This is to respect the fact that the top half of the file is indented
with spaces, while the bottom half is indented with tabs.
Graphics::reloadresources() is on the bottom half.
The previous way manually concatenated the first 7 characters of the
string together (and had an std::min() calculation). The new way instead
does std::string::substr(), which is much more snappy.
The actual unindent is done in a separate commit to minimize noise,
because diffs are terrible at clearly conveying unindents (it should put
all the minus lines together and all the plus lines together, too).
The entirety of the rest of scriptclass::loadcustom() is encased in a
block that first checks if the script with the name even exists. Instead
of indenting the rest of the function, just invert the check and reduce
indentation level.
This commit refactors custom level scripts to no longer be stored in one
giant vector containing not only every single script name, but every
single script's contents as well. More specifically,
scriptclass::customscript has been converted to an std::vector<Script>
scriptclass::customscripts (note the extra S), and a Script is just a
struct with an std::string name and std::vector<std::string> contents.
This is an improvement in both performance and maintainability. The game
no longer has to look through script contents in case they're actually
script names, and then manually extract the script contents from there.
Instead, all it has to do is look for script names only. And the
contents are provided for free. This results in a performance gain.
Also, the old system resulted in lots of boilerplate everywhere anytime
scripts had to be handled or parsed. Now, the boilerplate is only done
when saving or loading a custom level. This makes code quality much,
much better.
To be sure I didn't actually change anything, I tested by first saving
Dimension Open in current 2.3 (because current 2.3 gets rid of the
awful edentity whitespace), and then resaved it on this patch. There is
absolutely no difference between the current-2.3-resave and
this-patch-resave.
This enforces the C++03 standard for people making pull requests who may
not realize their fancy features are too new and shouldn't be used
(cough, cough, @leo60228).
I did some internet searching and this is what I got from this page:
https://crascit.com/2015/03/28/enabling-cxx11-in-cmake/
This resulted in two bugs:
1. Custom assets would not be unmounted when quitting to the menu.
2. Custom assets would be unmounted when playtesting a level.
The solution is to unmount assets in Game::quittomenu() instead.
Main game would retain custom level assets, now fixed. Also, custom fonts load properly. Finally, levels can be stored as a zip and placed in the levels folder, with the .vvvvvv file at the root of the zip and custom asset folders (graphics, sounds etc) also at the root.
Previously, the editor would always say it saved or loaded a level,
even if it was not successful. For example, because a file to load does
not exist, a file to save has illegal characters in its name or the
name is too long to be stored. Now failure is reported. Also, when
quitting the editor and saving before quitting is unsuccessful, the
editor will abort quitting.
I think it's a bit silly to always include the Steam and GOG APIs
whenever we build VVVVVV, since the only time they'll ever be used is in
a live build and not a dev build.
So now Steam and GOG are disabled by default. If you want them, you'll
need to add -DSTEAM=ON or -DGOG=ON respectively at CMake time. They're
also both automatically enabled for release builds.
This commit adds bounds checks to those commands in case say()/reply()
asks for more lines than there are left in the all-script-lines buffer
(not just the current script, so in order for it to segfault your script
has to be last in the all-script-lines vector), and in case text() asks
for more lines than there are in the rest of the rest of the parsed
internal script.
For some reason, scriptclass::loadcustom() sometimes refers to
`customscript` as `script.customscript` instead of `customscript`. The
`script.` is unnecessary.
Although it's not an issue, this should minimize the stack footprint of
calling scriptclass::load(), especially if it goes down to calling
scriptclass::loadcustom() or scriptclass::loadother().
Otherwise, it would end up indexing a vector out-of-bounds, which is UB
and causes a segfault, too. This is used in some constructs like
say(-1)
<internal command>
where the text contents don't matter, only that a text box shows up.
The `speak` command is the exact same as the `speak_active` command,
except without one line of code. So instead of copy-pasting the entire
thing, it's better to just combine them into the same chunk of code.
Instead each line is now held in a const char* array, like it should be.
This results in less work for the compiler, especially with
optimization, since every time the compiler encounters a constant
argument in a function, it has to go off and locate a place to put it.
But if we're upfront and say, hey, here's all the strings we ever need
conveniently packaged into one place, it'll be much more cooperative.
I have the feeling that none of the devs understood what extern did, and
they kind of just sprinkled it everywhere until things started working.
But like all other classes, it should just be one line in the class's
respective header file, and shouldn't be so messy.
When the game loads a room in a custom level, previously it would load
the tilemap of that room into ed.swapmap, and then mapclass::loadlevel()
would manually go through each element in ed.swapmap to set each tile in
`contents`. Why do that, when you can just return the vector from
editorclass::loadlevel() and set it directly? ed.swapmap is really
unnecessary.
It's a bit bad for the compiler if you have lots of function calls with
hardcoded strings in them, because every time the compiler encounters
one, it has to go out of its way to find a dedicated storage location
for the string, which is really inefficient. And it does this
inefficient thing every single time.
There's not much of an impact compiling these lists, but I at least want
to encourage this sort of code style, instead of the push_back(string)
style, in case we ever need a hardcoded array of things later.
Resetting game.activeactivity fixes triggering Undefined Behavior if you
quit to the menu while standing inside an activity zone, and then
re-entered the game. Resetting game.act_fade also fixes the activity
zone prompt fadeout that happens once the above segfault is fixed.
Don't worry, other activity-zone like variables such as teleporter
prompts and trophy text are already covered. obj.trophytext is reset in
scriptclass::hardreset(), too, and game.readytotele is reset every time
mapclass::gotoroom() is called, and mapclass::gotoroom() is called every
time a gamemode is started.
For some reason, the only way to get a cyan crewmate is by cycling
through an already-existing crewmate by keeping left-clicking on it.
This is because when you cycle through crewmate colors, the allowed
colors are 0-5, but when you place down a crewmate, it picks a random
color from 1-5, which seems to be a bit consistent.
So placing and cycling a crewmate now use the same color ranges.
Blackout mode doesn't work properly, because the game doesn't actually
black out the screen, it merely stops drawing things. Oh and only some
things at that, some other things are still drawn. This results in a
freeze-frame effect, which is apparently fixed in the Switch version.
Custom levels utilize this freeze-frame effect, so it's a bit annoying
that the "Game paused" screen draws on top of it and makes it so that
the freeze-frame freezes on "Game paused" until blackout is turned off.
So I'm making it so that "Game paused" doesn't get drawn in blackout
mode. However it will still do graphics.render() because otherwise it'll
just result in a black screen.
This fixes being able to trigger Undefined Behavior by pressing R when
not in-bounds in the Outside Dimension VVVVVV map, usually when you're
falling upwards towards Game Complete.
I also put bounds checks on normal roomdeaths for good measure. You'll
never know when you need it.
Instead of using somewhat-obtuse for-loops to initialize or reset these
vectors, it takes up less lines of code and is clearer if we use
std::vector::resize() and std::vector::clear() instead.
Since translucent roomname backgrounds were introduced in
TerryCavanagh/VVVVVV#122, it exposes one glaring flaw with the game that
until now has been kept hidden: in rooms with room names, the game
cheapens out with the tile data and doesn't have a 30th row, because the
room name would hide the missing row. As a result, rooms with room names
have 29 rows instead of 30 to fill up the entire screen. And it looks
really weird when there's nothing but empty space behind the translucent
room name background.
To remedy this, I added one row to each room with a room name in the level.
First, I had to filter out all the rooms with no room names. However, that's
actually all contained in Otherlevel.cpp, the Overworld, which contains 221
rooms (8 of which are the Secret Lab, 6 more of which are the Ship, so 207 are
the actual Overworld, right? Wrong, 2 of those Overworld no-roomname rooms are
in the Lab, so there are actually 205 Overworld rooms). The remaining level
data files all contain rooms with room names.
But the process wasn't that easy. I noticed a while ago that each room
contains 29 `tmap.push_back()`s, one for each row of the room, and each row is
simply a string containing the 40 tiles for that row, concatenated with
commas.
However, I decided to actually check my intuition by doing a grep on each
level file and counting the number of results, for example `grep 'push_back'
Labclass.cpp | wc -l`. Whatever number comes out should be divisible by 29.
That particular grep on Labclass.cpp returns 1306, which divided by 29 is 45
with a remainder of 1.
So what does that mean? Does that mean there's 45 rooms each, and 1 leftover
row? Well, not exactly. The extra row comes from the fact that Outer Space has
30 rows instead of 29. Outer Space is the room that comes up when the game
finds a room is non-existent, which shouldn't happen with a properly-working
game, except in Outside Dimension VVVVVV. In fact, each level file has their
own Outer Space, and every single Outer Space also has 30 rooms. So really,
this means there are 44 rooms in the Lab and one Outer Space room. (Well, in
reality, there are 46 rooms in the Lab, because 2 of them use the Outside
tileset but have no room names, so they're stored in Otherlevel.cpp instead.)
We find the same result for the Warp Zone. `grep 'push_back' WarpClass.cpp |
wc -l` returns 697, which is 24 remainder 1, meaning 23 rooms of 29 rows and 1
room of 30 rows, which corresponds with 23 rooms in the Warp Zone and one
Outer Space room.
However, Outside Dimension VVVVVV + Tower Hallways and Space Station 1 and 2
are both odd curiosities. Finalclass.cpp contains Outside Dimension VVVVVV,
(which is Intermission 1 and 2 and the Final Level), but also the Tower
Hallway rooms, i.e. the auxiliary Tower rooms that are not a part of the main
tower. Spacestation2.cpp contains both Space Station 1 and 2, so don't be
deceived by the name.
`grep 'push_back' Finalclass.cpp | wc -l` returns 1597, which is actually 55
remainder 2. So... are there two rooms with 30 rows? Yes, in fact, The
Gravitron and Outer Space both contain 30 rows. So there are actually 55 rooms
stored in Finalclass.cpp (not including the minitowers Panic Room and The
Final Challenge), 54 rooms of actual level data and one Outer Space room, and
breaking down the 54 rooms even further, 51 of them are actually in Outside
Dimension VVVVVV and 3 of them are Tower Hallways. Of the 51 Outside Dimension
VVVVVV rooms, 14 of those are Intermission 1, 4 of them are Intermission 2,
and the rest of the 33 rooms are the Final Level (again, not including the
minitowers).
`grep 'push_back' Spacestation2.cpp | wc -l` returns 2148, which is 74
remainder 2. Are there two rooms with 30 rows again? No; one of those counted
2148 rows is a false-positive, because there's an if-else in Prize for the
Reckless that replaces the row with spikes with a row without spikes if you
are in a time trial or in No Death Mode. So there's 73 rooms in Space Station
1 and 2, and one Outer Space room.
With all this in mind, I decided to duplicate the current last row of each
room, the 29th row, to add a 30th row. However, I wasn't going to do this
automatically! But neither was I going to write some kludge-y code to parse
each nightmare of a level file and duplicate the rows that way.
Enter: Vim macros! (Er, well, actually, I use Neovim.) I first did
`/push_back`, so that pressing `n` would keep going to the next `push_back` in
the file. Then I went to the 29th row of the first room in the file, did a
`Yp`, and then started my macro with `qq`. The macro went like this: `30nYp`,
which is simply going to the 29th row of the next room over and duplicating
it. And that's all there was to it. However, I had to make sure that (1) my
cursor was before the `push_back` on the line of the 29th row of the room, and
(2) that I didn't skip rooms, both of which were problems I encountered when
pressing Ctrl+Z a given invocation of the macro (the Ctrl+Z is just a
metaphor, you actually undo by typing `u` in Vim). And also I had to make sure
to be careful around the extra lines of `push_back`s in Prize for the Reckless
and The Gravitron, and make sure I didn't run past the end of the file and
loop back around. Thankfully, all Outer Space rooms are at the end of each
file.
But first, I had to increase the number of rows drawn in Graphics.cpp by 1 in
order to compensate for this, and do the same when reading the tile data in
Map.cpp. I had to change fillcontent(), drawmap(), drawfinalmap(),
drawtowermap(), and drawtowermap_nobackground(). Funnily enough, the tower
functions already used 30 rows, but I guess it's an off-by-one due to the
camera scrolling, so they now draw 31 rows each.
Then, I went in-game to make sure that the row behind each room name looked
fine. I checked EVERY single room with a room name. I turned on invincibility
mode and added a temporary line to hardreset() that always turned on
game.nocutscenes for a smoother playtesting experience. And to make sure that
rooms which have entirely empty bottom rows actually still have 30 rows,
instead of having 29 and the game assuming that the 30th row was empty
(because that sounds like it could lead to Undefined Behavior), I added this
temporary debugging line to the start of mapclass::fillcontent():
printf("(%i,%i) has %i rows\n", game.roomx, game.roomy, (int) tmap.size());
Everywhere I checked - and I made sure to check all rooms - every room had 30
rows and not 29 rows.
Unfortunately, some rooms simply couldn't be left alone with their 29th row
duplicated and had to be manually edited. This was because the 29th row would
contain some edge tiles because the player would be able to walk somewhere on
the 28th, 27th, and 26th rows, and if you duplicated said edge tiles behind
the room name, it would look bad.
Here's a list of rooms whose 30th rows I had to manually edit:
- Comms Relay
- The Yes Men
- Stop and Reflect
- They Call Him Flipper
- Double-slit Experiment
- Square Root
- Brought to you by the letter G
- The Bernoulli Principle
- Purest Unobtainium
- I Smell Ozone
- Conveying a New Idea
- Upstream Downstream
- Give Me A V
- $eeing Dollar $ign$
- Doing Things The Hard Way
- Very Good
- Must I Do Everything For You?
- Now Stay Close To Me...
- ...But Not Too Close
- ...Not as I Do
- Do Try To Keep Up
- Whee Sports
- As you like it
This is actually a strange case where it looked bad because of the 29th
row, instead of the 30th row, and I had to change the 29th row instead of
the 30th row to fix it.
- Maze With No Entrance
- Ascending and Descending
- Mind The Gap
Same strange case as "As you like it" (it's the 29th row I had to change
that was the problem, not the 30th).
- 1950 Silverstone Grand V
- The Villi People
I found that Panic Room and The Final Challenge also looked strange behind the
roomname background, but I can't do much about either because towers' tile
data wrap around at the top and bottom, and if I added another row to either
it would be visible above the room name.
I've considered updating the development editors with these new level tiles,
but I decided against it as the development editors are already pretty
outdated anyway.
This didn't cause any issues on the old push_back(string) system, but it
causes a huge alignment issue now. Don't know why these 7 zeroes are
here, but I'm fixing them.
It's a bit inconsistent how every time you toggle flip mode, it does
this flashing and shaking (and different SFX), regardless of whether or
not you're turning it on or off. To be more consistent with what happens
when you toggle screen effects, only turning on Flip Mode should do the
flashing/shaking/game-saved sound, and turning it off should just play
the Viridian squeak.
It's always personally irked me that the only time you get a Viridian
squeak when pressing ACTION to start fading out and going in-game is
when you start playing a custom level. And that's only if you don't have
a quicksave.
To make things more consistent (and to add more polish to the game), I
made sure there was a music.playef(11) every time game.mainmenu gets
set. I also made sure that this doesn't result in double-squeaking, i.e.
music.playef(11) being called twice, which can be very loud and
ear-hurting.
If you started a new game while having had a save (meaning you selectedd
"new game" while it wasn't in the same position as "continue"), then
saved and quit, your cursor will now end up at "continue" instead of
"new game". (If you didn't save, then your cursor would be out-of-bounds
and end up at position 0 anyway.)
For some reason (probably a copy-paste error), this XML tag gets atoi()
called on it before being assigned to Game::hardestroom. And only when
loading a quicksave, at that.
This would result in Game::hardestroom being set to an empty string,
which if you kept until Game Complete, would end up rendering as a
single null byte (if you even have a font face for said null byte).
I'm not sure how this error compiles in the first place, but whatever.
Whenever I compile with -O2, GCC gives me a warning that the return
value of fread() is being ignored. Which is a fair point, it means that
we're not doing proper error handling if something goes wrong. But I'm
also going to check the return value of fwrite() for good measure.
I believe that checking that this number is not equal to length is the
way to catch all errors and output an error message accordingly. I
didn't use ferror() or feof() mostly because I think it takes up too
much code. Also an error from fwrite() only says "Warning" because I
don't think there's much we can do if we don't fully write all bytes to
the intended file.
Previously, it was used to parse 30 strings whenever loading a room. But
now it's no longer used since we just assign the tilemap to the vector
directly.
They are now stored in const int arrays instead. Except for the Prize
for the Reckless room, which I made sure had its spikes removed in No
Death Mode and the Time Trial.
Instead, they're all stored in a constant int array.
I made sure The Gravitron still has 30 rows just like Outer Space,
though I don't think it matters.
Previously:
- Linux: xdg-open
- Everything else: open
Now:
- macOS and Haiku: open
- Everything else: xdg-open
This is all according to a comment by leo60228 in PR #203.
This change makes it so that in in-editor playtesting, the code to
handle pressing ENTER on activity zones is the same code to handle
pressing ENTER on activity zones in custommodeforreal.
This removes the need to copy-paste code and adapt it to in-editor
playtesting. And thus, this fixes an editor artifact where you can press
ENTER on activity zones while doing the death animation, even though you
can't do that when you're playing custom levels normally.
Since this is at the start of maprender(), the text boxes drawn by this
function will get hidden behind everything else being drawn on top of
it. Thus, it'll only result in a glitchy rendering where the text boxes
at the very top of the screen will be rendered in the roomname space of
the map screen.
To fix this rendering glitch, just remove the drawgui(). It's useless
anyway since it's being drawn behind everything else, so no need to have
it here.
We need to replace an "or" with an "and".
My best guess for this oversight happening was because of the weird
ordering. The code originally did "temp < 30" first and "temp > -30"
second instead of the other way around. With the weird ordering, it
becomes more natural to insert an "or" instead of an "and". So I swapped
around the ordering just for good measure.
This is also fixed in the mobile version.
This fixes horizontal and vertical warp backgrounds not resetting, and
also a bunch of other 1-frame glitches, most noticeably cutscene bars
and fadeouts.
This adds a new variable shouldreturntoeditor to Game to signal whether
or not it should return to editor at the end of the frame.
It's a bit annoying how vvvvvvman status is preserved between in-game
sessions, and the only thing reset is the color. This is annoying
because it means you have to close the game to reset vvvvvvman.
But now it'll be reset properly. I chose to put this reset code in
mapclass::resetplayer() instead of scriptclass::hardreset() because it
seemed like the more appropriate place. It's where all other properties
of the player are reset, after all.
This is to be extra safe and ensure that your hard-earned record isn't
lost at all.
In 2.2, the game didn't save your Super Gravitron record at all. It only
saved it if you closed the game by quitting to the title screen and
pressing ACTION on "quit game". You couldn't press Alt+F4, and you
couldn't press X, you had to do it that way, otherwise your record would
be lost.
In 2.3 right now, the game WILL save your unlock.vvv when you close the
game gracefully by any means, but this still means that if it doesn't
otherwise close gracefully (like, say, a crash), it won't save your
record. It feels like we shouldn't rely on this catch-all saving to save
Super Gravitron records.
Flag 67 seems to be a general-purpose Game Complete flag. It's used to
replace the CREW option with the SHIP option on the pause screen.
Unfortunately, it gets turned on too early during Game Complete. Right
when Viridian starts to teleport, you can bring up the pause screen and
select the SHIP option.
It will teleport you to the ship coordinates, but still keep you in
finalmode, and since the ship coordinates are at 102,111 (finalmode is
only around 46,54), you'll still be stuck in Outside Dimension VVVVVV,
and you've interrupted the Game Complete gamestate by switching to the
teleporting gamestate. Oh, and your checkpoint is set, too, so you can't
even press R. Oh and since there's no teleporter, and checkpoint setting
doesn't check the sentinel value, this results in Undefined Behavior,
too.
So this results in an in-game softlock. The only option you can do is
quit the game at this point.
To fix this issue, just move turning on flag 67 before the savetele() in
gamecompletelogic2().
This makes it so if you bring up the quit screen during a faded-out
screen, you will at least see the screen and won't see black. And also
the fade-in and fade-out animations will still work on the quit screen.
And more importantly, I tested and there's no 1-frame flicker or
anything.
This was used by the old system, which also had an over-reliance on
Terry's State Machine. And due to the fact that it relied on fademode,
it also meant that bringing up the pause screen while faded-out would
result in the player getting sent back to the menu, so one accidental
Esc press during a cutscene could mean countless hours of progress lost
(especially in custom levels).
This would cause the editor to think that it itself is in the middle of
fading to the menu, and so then will fade to the menu, thus losing any
unsaved work without warning.
Having to rely on a script means the fade out wouldn't be self-contained
in MAPMODE, which could cause a small issue where you could die during
the return to the lab. But that issue is now fixed. There's no need to
use the script, and anyway the endcutscene() and untilbars() in said
script don't do anything because there are no cutscene bars in the first
place, so no need to worry about those.
Again, what I've done here is removed the over-reliance on Terry's State
Machine to return to the lab, and just moved it into separate variables
instead. This means that returning to the lab is ALMOST entirely
self-contained in MAPMODE, except there's a quick jaunt over to GAMEMODE
to run a script because you can only run scripts in GAMEMODE.
Alright, so what I've done here is made exiting to the menu entirely
separate from Terry's State Machine, and thus it can now take place
entirely within MAPMODE instead of having to go back to GAMEMODE. Also,
it's faster by 15 frames since we don't need to wait for the map screen
to go back down.
This cleans up a whole lot of kludge variables, because this aggressive
hardreset() right as ACTION is pressed doesn't do anyone any favors.
This aggressive hardreset() was probably here because of the whole fact
that exiting to the menu uses Terry's State Machine, to minimize the
chances of interruption, but it actually causes more issues and allows
towers to interrupt the fadeout. And we should fix the root cause (the
usage of the state machine) instead of patching together some kludge.
I don't want the quit code to only be in the state machine, but I'll
keep the gamestate around for compatibility reasons (there are custom
levels that directly use gamestate 80 to quit to the menu).
Due to the previous commit, these will no longer be regularly taking in
out-of-bounds entity indices. Or at least they shouldn't, so I'm putting
in these print statements here on the off-chance that they do.
Otherwise, this would result in the game updating an entity twice, which
isn't good. This is most noticeable in the Gravitron, where many
Gravitron squares are created and destroyed at a time, and it's
especially noticeable during the part near the end of the Gravitron
where the pattern is two Gravitron squares, one at the top and bottom,
and then two Gravitron squares in the middle afterwards. The timing is
just right such that the top one of the two middle ones would be
misaligned with the bottom one of the two when a Gravitron square gets
outside the screen.
To do this, I changed entityclass::updateentities() into a bool, and
made every single caller check its return value. I only needed to do
this for the ones preceding updateentitylogic() and
entitymapcollision(), but I wanted to play it safe and be defensive, so
I did it for the disappearing platform kludge, as well as the
updateentities() within the updateentities() function.
Apparently, the game just leaves minitowermode on, which can cause
issues if you're in a horizontal warping room (and not any other room
type, due to how frame ordering works). This would manifest itself as a
wrong warp to (20,20) because the game is trying to apply the hardcoded
minitower screen transitions when it shouldn't be.
The main ones to beware of here are entityclass::updateentities(),
entityclass::updateentitylogic(), and entityclass::entitymapcollision().
They would index out-of-bounds and thus commit Undefined Behavior if the
entity was removed in entityclass::updateentities(). And it would've
been fine enough if I only added bounds checks to those functions.
However, I decided to be a bit more defensive and play it safe, and
added bounds checks to ALL functions taking in not only an entity
indice, but also blocks and linecrosskludge indices.
Otherwise, if you died after entering a room with a horizontal or
vertical warp background (but not the all-sides warp background), the
warp background would be the first thing you see when going to the Game
Over screen, and would then start scrolling downwards with the proper
tower background coming in from the top.
This oversight seems to have always been in the game.
Was No Death Mode actually tested? Like, did anyone ever play through
the entire game without dying in the Warp Zone, or even AFTER completing
the Warp Zone, like, ever?
When you complete the game, you're now redirected to the play menu. This
is because your quicksave will have been deleted so you can't go back to
the summary menu.
When you complete a custom level, you'll go back to the levels list, in
case you started the level from a quicksave.
Looks like there wasn't a custommode check for the spawning of crewmates
based on which crewmates were rescued, but now there is.
This has gone undiscovered for a long time, mostly because people don't
use the rescued() internal command.
While I was working on my over-30-FPS patch, I found out that the tower
background in the credits scroll was being completely re-drawn every
single frame, which was a bit wasteful and expensive. It's also harder
to interpolate for my over-30-FPS patch. I'm guessing this constant
re-draw was done because the math to get the surface scroll properly
working is a bit subtle, but I've figured the precise math out!
The first changes of this patch is just removing the unconditional
`map.tdrawback = true;`, and having to set `map.scrolldir` everywhere to
get the credits scrolling in the right direction but make sure the title
screen doesn't start scrolling like a descending tower, too.
After that, the first problem is that it looks like the ACTION press to
speed up the credits scrolling doesn't speed up the background, too. No
problem, just shove a `!game.press_action` check in
`gamecompletelogic()`.
However, this introduces a mini-problem, which is that NOW when you hold
down ACTION, the background appears to be slowly getting out of sync
with the credits text by a one-pixel-per-second difference. This is
actually due to the fact that, as a result of me adding the conditional,
`map.bscroll` is no longer always unconditionally getting set to 1,
while `game.creditposition` IS always unconditionally getting
decremented by 1. And when you hold down ACTION, `game.creditposition`
gets decremented by 6.
Thus, I need to set `map.bscroll` when holding down ACTION to be 7,
which is 6 plus 1.
Then we have another problem, which is that the incoming textures desync
when you press ACTION, and when you release ACTION. They desync by
precisely 6 pixels, which should be a familiar number. I (eventually)
tracked this down to `map.bypos` being updated at the same time
`map.bscroll` is, even though `map.bypos` should be updated a frame
later AFTER updating `map.bscroll`.
So I had to change the `map.bypos` update in `gamecompleteinput()` and
`gamecompletelogic()` to be `map.bypos += map.bscroll;` and then place
it before any `map.bscroll` update, thus ensuring that `map.bscroll`
updates exactly one frame before `map.ypos` does. I had to move the
`map.bypos += map.bscroll;` to be in `gamecompleteinput()`, because
`gamecompleteinput()` comes first before `gamecompletelogic()` in the
`main.cpp` game loop, otherwise the `map.bypos` update won't be delayed
by one frame for when you press ACTION to make it go faster, and thus
cause a desync when you press ACTION.
Oh and then after that, I had to make the descending tower background
draw a THIRD row of incoming tiles, otherwise you could see some black
flickering at the bottom of the screen when you held down ACTION.
All of this took me way too long to figure out, but now the credits
scroll works perfectly while being more optimized.
I had forgotten that the game flashed and did screen-shaking when you
pressed ACTION to quicksave.
While testing for my over-30-FPS patch I stumbled across this.
It's less code being copied and pasted, especially since for my
over-30-FPS patch I would have to make a separate function for each if
both of them were still there, but if they're unified into one then I
will only have to make one more function.
And since map.scrolldir is now used outside of GAMEMODE, we'll need to
reset it in hardreset() and when exiting playtesting.
Due to the previous commit, the descending tower background now has to
account for map.bscroll, or else it will be off by one pixel from the
incoming textures. But ascending tower backgrounds work fine, so no need
to do anything with those.
Looks like this was done as a quick fix instead of taking the time to
figure out the math needed to actually draw the incoming textures, which
is fair enough - it only makes one room, Panic Room, slightly laggier.
While I was working on my over-30-FPS patch, though, I came across the
fact that this background kept getting entirely redrawn every frame, and
it seems like it would be easier to interpolate descending tower
backgrounds if we scrolled what was already there instead.
Here, we have to draw two rows of incoming textures, otherwise the
scrolling surface will produce black lines.
Previously, if you had backgrounds disabled in accessibility options,
and went to the editor and opened up the editor menu, it would be drawn
straight on top of what was already there in the editor instead of being
drawn on top of black. So now it's drawn on top of black.
I want exiting No Death Mode to go back to the "play modes" menu, not to
the "start game" menu, because it's too far back. Also do the same if
you either die or complete No Death Mode.
Also I initialized Game::wasinintermission, probably a good thing to
initialize variables.
During testing, I made a cursed level that set the flash timer to
precisely 1,000,000 frames. It turns out that if I activated the timer
in playtesting, exited playtesting, and exited the editor without ever
re-entering playtesting, the timer still kept going. So to prevent being
able to do that, we should hardreset() when exiting the editor.
In-game because that's where screen effects are used the most. But on
the title screen, screen effects are used when you press ACTION to start
the game, and when you enable screen effects, too.
Otherwise, we don't need screen effects for any other game-gamestate.
This de-duplicates the screen effects rendering code by putting it
inside a function, Graphics::renderwithscreeneffects(), and using that
instead of copy-pasted code.
The code to decrement the timers for flashing and shaking is now handled
outside the game-gamestate case-switch, instead of having to be
duplicated inside each render function.
As a bonus, I made it so the timer decrements even if screen effects are
disabled. This is to prevent any theoretical situation where the timer
can "pile up" due to disabled screen effects not letting it tick down.
This removes a lot of duplicate code, which towerrender() mostly
consisted of, even though the only difference is that it draws a
separate map and screen edge spikes are drawn.
This removes lots of duplicated code that drawtowerentities() did,
because all that really changed was accounting for map.ypos (which can
be done conditionally) and where and when the room wrapped (which can
also be done conditionally).
This fixes an oddity that's only visual, which could only happen in
custom levels by using the createentity() internal command.
For the same reason that the second through fourth tiles of moving
platforms on the top and left was buggily rendered, SDL_BlitSurface()
strikes again to mutate the SDL_Rect we pass it and render the next
SDL_BlitSurface() call inbounds, even though we don't need it to.
Previously, the game could end up rendering a warping sprite twice due
to the fact that it could run "if entity is on the right side of the
screen" right after "if entity is on the left side of the screen" (but
not the other way around). This is most noticeable if you have a custom
player sprite with translucent pixels and stand on the left side of a
warping screen, but the code suggests it happens when warping through
the top of the screen, too.
Instead of doing
if (!obj.entities[i].invis)
{
...
}
It's better to do
if (obj.entities[i].invis)
{
continue;
}
...
It reduces the indentation by one level, which is always a good thing.
In the last commit, I removed having the flip mode conditional directly
inside the sprite-drawing code for each size type, which would reduce
the indentation one level. However, I opted to hold off un-indenting
until this commit, otherwise it would've produced too much noise.
The game uses flipsprites.png instead of sprites.png when in flip mode,
mostly to add exceptions for sprites that SHOULDN'T be flipped in flip
mode.
Looks like to achieve this, the routines for sprite drawing got
copy-and-pasted every single time flipsprites.png needed to be
conditionally used, resulting in large amounts of copy-pasted code. And
this copy-pasted code resulted in copy-paste errors, with relating to
VVVVVV-Man, because apparently due to two copy-pasting errors, the
combined giant crewmate in the epilogue uses flipsprites.png even if you
aren't in flip mode, and it also uses the width instead of the height of
sprites_rect when in flip mode (although, this doesn't end up mattering,
but still).
The solution here is to simply change the referenced sprites vector to a
pointer that can conditionally change based on the game being in flip
mode or not.
Looks like I forgot to test that my music silencing patch didn't break
the music being silent during the "You have found a shiny trinket" and
"You have found a shiny crewmate" text boxes. So I've added a check for
game.completestop in the music handling in main.cpp.
Found this bug while I was testing my towerlogic/gamelogic merge patch.
The problem here is that we're directly using the C stdio library,
instead of using PHYSFS's stuff. So I've added a function
FILESYSTEM_delete() that does exactly that.
This commit fixes a slightly frustrating thing where if you start a new
game, and then exit before saving, "start game" will always take you to
a new game, even though you have unlocked things like the Secret Lab or
Time Trials.
Now, if you select "new game" (only possible if you have something
unlocked), then quit before saving, "start game" will still take you to
the play menu, but "continue" is replaced with "start" and "new game" is
gone.
This will be a useful shorthand to ask "do we have the Secret Lab, or
any Time Trial, or Intermission replays, or No Death Mode, or Flip Mode
unlocked?"
This fixes being able to rack up a large amount of stack frames by
pressing Esc repeatedly in the editor, which would be a problem if you
were to then return to the main menu afterwards.
Instead, if Menu::ed_settings is already in the stack, the game will
simply return to that menu instead of creating it. Else, it will just
create the menu.
Also, as extra attention to detail, I made sure that the menu create or
return only happens if Esc opens the settings menu, and not when Esc is
closes it.
This stabilizes the code that handles the menu that you land on if you
press Esc and quit to the menu.
Instead of using Game::returnmenu(), we now use the new function
Game::returntomenu() to clearly express intent that we want to return to
a specific menu. So I've added another kludge variable
Game::wasinintermission for the was-in-intermission case.
Also, I made it so that if you didn't have a main game telesave or
quicksave, you just get brought back to the main menu. Because you
shouldn't be able to go to the play menu without a quicksave or
telesave.
When exiting from a game-gamestate which may have been entered through a
varying amount of menus, the solution is to not use Game::returnmenu(),
and to instead have a way to go back to a certain given menu.
This commit fixes a bug where the second, third, and fourth tiles of
moving platforms would render offset from the first tile if the moving
platform hit the top or left edge of the screen.
This is due to the fact that SDL_BlitSurface() will end up changing the
coordinates of the rectangle we pass to it to be 0 if they're negative,
but only after it's already been drawn. Previously, we kept re-using the
same rectangle each time we drew each segment of the moving platform,
but since it only changes the draw rectangle after it's already been
drawn, the first tile shows up fine, but not the rest of the tiles,
hence resulting in an offset.
To fix this, we do the same thing as we did for drawing the "really big
sprite" (size-type 9): just reset the rectangle we use every time we
draw a segment of the moving platform.
They're literally the exact same thing, except one is 4 and the other
has an 8. No need to have all that copy-pasted code.
Actually, the only other difference was that size-type 8 set the
drawRect to sprites_rect instead of tiles_rect for some reason? Even
though it doesn't matter, anyway, because SDL_BlitSurface() only cares
about the X and Y you pass it, not the width and height, which is the
only difference between tiles_rect and sprites_rect.
To make sure that people wouldn't wonder where size-type 8 went if they
saw a blank space between size-type 7 and size-type 9, I kept the
size-type 8 conditional, but inside it is just a comment telling you to
go to size-type 2.
Instead of copy-pasting all the BlitSurfaceStandard()s all over again,
just make the referenced vector a pointer that changes depending on
map.custommode.
There's a noticeable seam in the horizontal and warp backgrounds
whenever you enter a new room. Entering a new room triggers the game to
re-draw the entire warp background instead of simply scrolling what it
already has. This seam is the result of the initial background draw
being misaligned with the rest of the scrolling.
If you get out your measuring tools, you'll see that it's misaligned by
exactly 3 pixels (this applies to both horizontal and vertical warping).
If you look at the part of the code where the game draws fresh warping
textures after scrolling the existing ones offscreen, you'll see it
starts with an offset of 317, which is exactly 320 minus 3. And for
vertical, it uses 237, which is exactly 240 minus 3.
This is where the misalignment comes from. Since the incoming textures
are drawn 3 pixels to the left, but the initial draw isn't, this results
in a misalignment and causes the seam.
To fix this, draw the initial draw of the horizontal and vertical warp
backgrounds 3 pixels to the left.
It looks like one bracket got out-of-place for whatever reason. This
doesn't affect the case-switch at all, due to how case-switches work,
but it's still weird to look at.
Indentation has been updated accordingly.
Some custom levels have their own custom music and sync that music to
scripted cutscenes, which is actually pretty impressive. However,
they've always run into a small thorn, which is that you can easily
desync the music by unfocusing the game, because the audio will keep
playing when the game is unfocused.
This should remove that thorn by pausing the audio on unfocus, and
resuming when focused, so that the music can no longer desync, but you
can still pause the game by unfocusing it.
This is yet another feature in VCE that hasn't been upstreamed until
now.
It looks like this variable was originally intended to keep track of th
volume of the game, but then it was used as a boolean in main.cpp to
make sure the game didn't call Mix_Volume() and Mix_VolumeMusic() every
frame.
However, it is now a problem, because I put the music mute handling code
in the very branch that game.globalsound protects against, but since
game.globalsound is here, if I mute the music, then mute the whole game,
then unmute the music, and then unmute the whole game, sound effects
will no longer be muted but the music will still be muted, until I mute
and unmute the whole game again. This is annoying and inconsistent, so
I'm removing this check from the 'if (!game.muted)' branch.
Plus, given that the Mix_VolumeMusic() and Mix_Volume() calls happen
every frame if the game is muted anyways, it doesn't seem to be a
problem to call these every frame.
These do basically nothing. The only time they're used is
getGlobalSound() in an if-statement in main.cpp, but all that
if-conditional does is call setGlobalSound() anyway, which is something
that doesn't really have any side effects. So I'm removing these vars to
simplify the code.
This is for people who want to use their own soundtrack while playing
the game, but who don't want to mute the sound effects as well.
This feature was added to VCE, but it was added in the strangest way. It
was made an option in "game options" instead of being a keybind, and I
don't know why.
The environment variable SteamTenfoot corresponds with the game running
in Steam Big Picture mode or SteamOS if it is defined. There's a
certification process for both full controller support and Big Picture
mode, and being able to launch a file window in Big Picture mode is an
instant cert failure.
Have to add some includes and put these behind some ifdefs, of course.
I'm pretty sure FreeBSD and OpenBSD and Haiku are POSIX enough that the
"open" command will work on them, too.
I would've loved to make FILESYSTEM_openDirectoryEnabled a simple bool
instead of a function, but I ran into issues with putting it in the
FileSystemUtils header file, so I'll just make it a function and call it
a day.
This fixes a bug where levels in the levels list duplicate if there's an
invalid file (such as a folder) in the levels directory.
It looks like it happens because we don't free the memory if
PHYSFS_readBytes() encounters an error, even though we should. Then we
get into Undefined Behavior territory and end up reusing memory, and
here it just happens that previously, parsing the entire XML document
for each level file was enough to make the loaded file pointer point to
garbage that would fail the metadata check, but if we optimize it so we
don't parse the entire XML document, it starts reusing memory instead.
To find each individual tag quickly, to optimize levels list loading.
I opted to not read the tags <Created>, <Modified>, and <Modifiers> as
they're actually pretty useless.
Also I've added a tag finder for <MetaData> but it's not meant to be
used directly, it's only used to check that the tag exists.
This turns the implicit-fallthrough warning into a full compile-time
error.
Implicit fallthrough is when you forget a break statement in a
case-switch, thus letting one case fall through into the next case and
causing debugging headaches.
This is different from the good type of fallthrough that you use to have
one case with multiple different names, like so:
case 0:
case 1:
case 2:
In that case, it's obvious that you want to have fallthrough there.
The problem was, if you were in a time trial and quit, it wouldn't go
back to selecting your current time trial. But also if you were in a
custom level and quit, you would still be on the playerworlds menu.
The problem was twofold: first, I simply wasn't doing the custommode
check. But secondly, I couldn't use map.custommode directly, because
whenever you quit the game aggressively hardreset()s everything
immediately when you press ACTION.
There's probably a good reason for that aggressive hardreset(), so I
won't touch that hardreset() in any way. Instead, I had to introduce two
kludge variables wasintimetrial and wasincustommode to Game, and use
those to do the check proper.
This makes it more convenient if you have a large levels directory, as
some people in the VVVVVV custom levels community do.
On the first page, this option will change to be "last page" instead.
Since the addition of another menu option pushes up the list of levels
too close to the selected level data itself, I've had to move the list
of levels down by 4 pixels (but "next page"/"previous page"/"return to
menu" are still in their same position).
This feature was already added to VCE but hasn't been upstreamed until
now.
This also replaces some createmenu()s with returnmenu()s as needed even
when said createmenu()s already didn't go to the main menu.
Now when you exit the level editor, you'll be selecting the "level
editor" option in "play levels", and if you exit from a level you'll
still be selecting that level in the levels list.
Furthermore, regardless of what you're exiting, your cursor position
will be remembered.
This is to not reset your cursor position every time you return on
something. It's also to automatically keep track of which menu was the
previous menu instead of manually hardcoding said previous menu.
You were able to mismatch the color of the quicksave/telesave summary
and the text/background by pressing Esc when in the "continue" menu,
then pressing ACTION on "no, return".
This commit fixes that bug by putting the map.settowercolour(3) inside
the Menu::continuemenu creation code itself. However, since the
Menu::youwannaquit code does map.nexttowercolour() right after it does
the game.createmenu(), we also need to put the map.nexttowercolour()
before the game.createmenu() beforehand so it doesn't mess up the cyan
color that Menu::continuemenu sets.
Additionally, I removed the map.settowercolour() from the input handling
of Menu::play, as it's superfluous.
This marks pressing ACTION on "next page" in the levels list, credits,
pressing ACTION on "continue" in "You have unlocked" menus, and pressing
ACTION on an unlock option in the unlock menu and time trial unlock menu
as being the same menu.
This is to prevent creating unnecessary stack frames when using said
menu options in those menus.
These aren't necessary, the menu will update regardless. There isn't
even such a call for the mouse cursor toggle option, that's how
unnecessary it is.
Unless it's the main menu, or unless it's not the same menu. Whether or
not the menu is the same is left up to the caller, because some menus
could be the same but use different names, so we can't simply
automatically check that the names are different and assume that they
aren't the same menu.
This temp variable isn't used anywhere else, and even if it was it's set
to something every time it's used, so there's no risk of this commit
breaking any backwards compatibility.
I assume it was so a dev could mark the spot where they needed to put in
the analogue toggle, and they found a unique yet easy to remember
sequence of characters to Ctrl+F as a marker.
Looks like it was a remnant from the Flash days, and the "delete your
saves if you want to use slowdown" was a bit too mean so it stopped
being a thing in the C++ version.
Much more stylistic, you don't need to repeat "game.currentmenuname" for
each case, and you don't need to deal with the dangling first "if" that
doesn't have an "else".
I presume it was meant to have the text of the currently-selected menu
option inside it, before the code switched over to using the indice of
the currently-selected menu instead? Would've been more error-prone to
use the text name directly.
Stringly-typed things are bad, because if you make a typo when typing
out a string, it's not caught at compile-time. And in the case of this
menu system, you'd have to do an excessive amount of testing to uncover
any bugs caused by a typo. Why do that when you can just use an enum and
catch compile-time errors instead?
Also, you can't use switch-case statements on stringly-typed variables.
So every menu name is now in the enum Menu::MenuName, but you can simply
refer to a menu name by just prefixing it with Menu::.
Unfortunately, I've had to change the "continue" menu name to be
"continuemenu", because "continue" is a keyword in C and C++. Also, it
looks like "timetrialcomplete4" is an unused menu name, even though it
was referenced in Render.cpp.
I think it's about time that this number be updated, yeah? This isn't to
say that 2.3 is finished or almost finished or anything, this is just to
clearly differentiate that this isn't 2.2.
If you have invincibility mode or slowdown enabled, the game will not
let you select the Secret Lab, Time Trials, or No Death Mode. To make
this clearer, this commit grays out said options if they are disabled
for that reason.
The game won't let you select the Secret Lab if you're in invincibility
mode, probably so you can't set illegitimate Super Gravitron records
just by standing there and doing nothing.
However, for some reason, it'll still let you select the Secret Lab even
if you've slowed down the game. For consistency, let's prevent selecting
the Secret Lab if the game isn't running at fullspeed, too.
I've converted every "else if"-chain in menu render/input code to be a
case-switch, except for the levels list, the "game options" menu
(because it has the MMMMMM menu option which isn't a compile-time
constant), and the "play" menu (because it has the Secret Lab menu
option which also isn't a compile-time option).
I also did NOT convert some case-switches relating to unlocks in
Input.cpp, mostly because they use a system where the "if we have this
unlocked" conditional is a part of the "if this is the current menu
option" conditional, and they use the 'else' branch to play a sad sound
if that "if we have this unlocked" conditional fails.
I've also converted the game.gameframerate and game.crewrescued() "else
if"-chains to be case-switches instead.
There was one level that was indented with 2 spaces instead of 4, which
made everything else look weird. Then some lines were randomly indented
further for no reason.
Doing this before the next commit is done so as to not make the next
commit noisier.
This removes duplicate code that came about as a result of various
possible permutations of menu options, depending on being M&P, having no
custom level support, having no editor support, and having MMMMMM.
The menus with such permutations are the following:
- main menu
- "start game" is gone in MAKEANDPLAY
- "player levels" is gone in NO_CUSTOM_LEVELS
- "view credits" is gone in MAKEANDPLAY
- "game options"
- "unlock play data" is gone in MAKEANDPLAY
- "soundtrack" is gone if you don't have an mmmmmm.vvv file
- "player levels"
- "level editor" is gone in NO_EDITOR
I achieve this de-duplication by clever use of calculating offsets,
which I feel is the best way to de-duplicate the code with the least
amount of work, if a little brittle.
The other options are to (1) put function pointers on each MenuOption
object, which is pretty verbose and would inflate Game::createmenu() by
a lot, (2) switch all game.currentmenuoption checks to instead check for
the text of the currently-selected menu option, which is very
error-prone because if you make a typo it won't be caught at
compile-time, (3) add a unique ID to each MenuOption object that
represents a text but will error at compile-time if you make a typo,
however this just duplicates all the menu option text, which is more
code than was duplicated previously.
So I just went with this one.
I don't know why you would have to have both defined simultaneously, but
now you can.
The compile fail was caused by the fact that if both were defined, then
there would be an expression like this in Map.cpp:
switch (t)
{
case 0:
}
which is an invalid expression.
The solution is to put 'case 0:' into the MAKEANDPLAY ifdef-guard as
well.
Just like I moved the menu ACTION press handler, I'm doing this as well.
It only removes one level of indentation, but it makes titlerender()
easier to understand.
Just like before, I have to put the separate function first, else
titlerender() won't know what it is.
Previously, the code looked something like:
else { if (...) {...} else { if (...) {...} else { etc. } }
And kept indenting every time there was an else-if.
This commit puts all else-ifs on the same indentation level, so it
doesn't slowly push the code to the right.
This takes out 3 indentation levels from the ACTION press handling,
making titleinput() easier to read as a whole.
Unfortunately, we have to put menuactionpress() first, even though I'd
want it the other way around, otherwise titleinput() won't know what it
is.
If we need it (which I don't think we will be anytime soon) we can
always just get it back through source control. Otherwise, it simply
gets in the way.
Firstly, menu options are no longer ad-hoc objects, and are added by
using Game::option() (this is the biggest change). This removes the
vector Game::menuoptionsactive, and Game::menuoptions is now a vector of
MenuOption instead of std::string.
Secondly, the manual tracker variable of the amount of menu options,
Game::nummenuoptions, has been removed, in favor of using vectors
properly and using Game::menuoptions::size().
As a result, a lot of copy-pasted code has been removed from
Game::createmenu(), mostly due to having to have different versions of
menus depending on whether or not we have certain defines, or having an
mmmmmm.vvv file inside the VVVVVV directory. In the old days, you
couldn't just add or remove a menu option conveniently, you had to
shuffle around the position of every other menu option too, which
resulted in lots of copy-pasted code. But now this copy-pasted code has
been de-duplicated, at least in Game::createmenu().
This removes map.numshinytrinkets in favor of using
map.shinytrinkets.size(). Having automatic length tracking is much less
error-prone and less tedious.
It's treated like a bool anyway, so might as well make it one.
This also necessitates updating every single instance where it or an
element inside it is used, too.
Looks like it was here from that arg passing stuff from earlier, as a
workaround to not pass args around. Well, there's no need to create an
extra UtilityClass now either, just use the one in the global namespace.
Previously, it existed solely to count the number of trinkets and
crewmates when loading a level, because we were keeping track of the
amount of them manually, incrementing and decrementing every time a
trinket or crewmate was added or removed, but loading a new level
represented a case that could potentially not be an increment or
decrement.
However, since the amount tracking is now handled automatically, this
function now does nothing, and can be safely removed.
Same principle as removing the separate variable to track number of
collected trinkets. This means it's less error-prone as we're no longer
tracking number of trinkets separately.
In the function that counts the number of trinkets, I would've liked to
have used std::count_if(). However, the most optimal way would require
using a lambda, and lambdas are too new for the C++ standard we're
using. So I just bit the bullet and counted them manually.
game.trinkets is supposed to be correlated with obj.collect, however why
not just count obj.collect directly?
This turns game.trinkets into a function, game.trinkets(), which will
directly count the number of collected trinkets and return it. This will
fix a few corner cases where the number of trinkets can desync with the
actual collection statuses of trinkets.
In order to keep save compatibility with previous versions of VVVVVV,
the game will still write the <trinkets> variable. However, it will not
read the <trinkets> variable from a save file.
It's a bit rude to put the user back at the main menu after toggling
something. Maybe they also wanted to do something else in the menu while
they're toggling MMMMMM, there's no reason to immediately put them back
there.
Whenever you collect a trinket, game.stat_trinkets gets updated (if
applicable) to signify the greatest amount of trinkets you have ever
collected in the game. However, custom levels shouldn't be able to
affect this, as their trinkets are not the same trinkets as the main
game.
It turns out that when the game warps moving platforms, it won't remove
the block from the position before they warped. Eventually, these blocks
will pile up and will never be removed, causing a memory leak.
I noticed that the code for going to the adjacent room when offscreen
and for warping instead if the room is warping was a bit
copy-and-pasted. To clean up the code a bit, there's now 5 separate
checks in gamelogic():
if (map.warpx)
if (map.warpy)
if (map.warpy && !map.warpx)
if (!map.warpy)
if (!map.warpx)
I made sure to preserve the previous weird horizontal warping behavior
that happens with vertical warping (thus the middle one), and to
preserve the frame ordering just in case there's something dependent on
the frame ordering.
The frame ordering is that first it will warp you horizontally, if
applicable, then it will warp you vertically, if applicable. Then if you
have vertical warping only, that weird horizontal warp. Then it will
screen transition you vertically, if applicable. Then it will screen
transition you horizontally, if applicable.
To explain the weird horizontal warp with the vertical warp: apparently
if an entity is far offscreen enough, and if that entity is not the
player, it will be warped horizontally during a vertical warp. The
points at which it will warp is 30 pixels farther out than normal
horizontal warping.
I think someone ran into this before, but my memory is fuzzy. The best I
can recall is that they were probably createentity()ing a high-speed
horizontally-moving enemy in a vertically warping room, only to discover
that said enemy kept warping horizontally.
As a result of the previous commit, textboxclass::clear() is now unused.
textboxclass::firstcreate() was already useless. So remove both those
functions and initialize the values in the textboxclass constructor.
This removes the variables graphics.ntextbox, as well as removing
'active' from each text box object. Thus, all text boxes are really
real, and you don't have to check its 'active' variable.
Something that's slightly annoying is that in order to make the vector
of text boxes be properly used, the text box cannot remove itself.
Because the text box does not know it's in a vector. So move the removal
of the text box to drawgui() instead.
Just like earlier, these are of the form
if (cond1) { if (cond2) { if (cond3) { thing; } } }
and are really annoying to read.
Also this removes the remnants of the 'active' system that have been
replaced with 'if (true)' conditionals in order to not add noise to the
diff.
Previously, it was used in order to clear a block and deactivate it, and
the constructor function simply called clear() in order to not duplicate
code. However, clear() is no longer necessary (just remove the block
from the blocks vector), and so we can put initialization right back in
the constructor function.
It turns out the game engaged in pseudo-UB when removing activity zones,
which got turned into actual UB due to the previous commit.
There were three places where this could happen:
- Pressing ENTER on an activity zone in normal gameplay
- Pressing ENTER on an activity zone in in-editor playtesting (because
the code is duped here)
- Pressing ESC and quitting to menu while standing inside an activity
zone
In all cases, game.activeactivity would still be pointing to a
non-existent activity zone. This activity zone in the previous system
would simply be a block with a false 'active', and in the system where
C++ vectors are used properly, would index past the blocks array.
In fact, it is a bug that when you press ENTER on an activity zone, the
activity zone prompt suddenly turns to black, then immediately
disappears. It was pointing to a block that had its clear() method
called, which is why it was all black, and it was an inactive block!
This commit makes it so pressing ENTER on an activity zone smoothly
fades out the activity zone prompt instead of being sudden black.
This removes the variables obj.nblocks, as well as removing the 'active'
attribute from the block object. Now every block is guaranteed to be
real without having to check the 'active' variable.
Removing a block while iterating now uses the removeblock_iter() macro.
Previously there was an entclass::clear(), and initialization of an
entclass was done by calling clear() in order to not duplicate code. But
now there's no need for an entclass::clear(), and it is in fact unused
(just call entityclass::removeentity() instead), so I'm removing this
function.
I guess these were here earlier when there were 'active' conditionals,
but then I removed those, so now they look weird next to the 'i != j'
conditionals, so I'm removing them.
These would be of the form
if (cond1) { if (cond2) { if (cond3) { thing; } } }
which is really annoying to read and could've been written as
if (cond1 && cond2 && cond3) { thing; }
so that's what I'm fixing here.
There will be another commit later that fixes this but in places related
to blocks.
Not sure why this function is here. It makes sense if you know that the
game will only do certain moving platform things if you already have a
moving platform in the room, however apparently this function has
absolutely nothing to do with it.
This function's sole purpose was to make sure obj.nentity was in sync,
and that obj.nentity-1 pointed to the last 'active' entity in
obj.entities. But now that obj.nentity is removed and we use
obj.entities.size() instead, it is no longer necessary.
In the previous commit, if an if-statement consisted solely of checking
the active attribute of an entity, I temporarily changed it to 'true'
and put a comment to remove it later, because it would add too much
noise to unindent everything in the same commit.
This removes the variables obj.nentity and obj.nlinecrosskludge, as well
as removing the 'active' attribute from the entity class object. Now
every entity you access is guaranteed to be real and you don't have to
check the 'active' variable.
The biggest part of this is changing createentity() to modify a
newly-created entity object and push it back instead of already
modifying an indice in obj.entities.
As well, removing an entity now uses the new obj.removeentity() function
and removeentity_iter() macro.
Also when we switch everything to not use 'active', we'll need this
macro to remove entities while iterating forwards through the vector one
at a time.
Ok, once we switch everything over to not use the 'active' system, it's
easier to read removeentity(t) than it is to read
entities.erase(entities.begin() + t).
This moves the setenemyroom() function onto the entity object itself, so
I can more easily change all 'entities[k].' to 'entity.' in
entityclass::createentity() later.
Additionally, I've had to move the rn() macro from Entity.h to Ent.h, or
else entclass::setenemyroom() won't know what it is.
This moves the setenemy() function onto the entity object itself,
instead of having to give the indice of the entity in obj.entities. This
makes the code more object-oriented so later I can simply change all
'entities[k]' to 'entity.' in entityclass::createentity().
It is an exact duplicate of musicclass::haltdasmusik(), so use that
function instead and update callers. Looks like
musicclass::haltdasmusik() came first, anyway (musicclass::stopmusic()
was only used in editor.cpp).
Looks like musicfade is an unused variable, anyway. I might remove it,
but I have some plans in the future that involve repairing what it was
intended for, so I'll hold off on removing it (and some other unused
variables in Music.cpp) for now.
As discussed earlier, some custom levels have taken advantage of the
fact that songs 0 and 7 loop and also fade in when using PPPPPP while
having an mmmmmm.vvv file present.
The problem is that it would index out-of-bounds if you did this, but
this UB hasn't caused an exception until my change to refactor
script-related vectors by removing their separate length-trackers.
Most of the code was already commented out, and those comments were
removed in earlier commits, but this removes all recording variables
from Game and simplifies the game-gamestate handling in main.cpp a
little bit.
Just a miscellaneous code cleanup.
There's no glitches that take advantage of the previous situation,
namely that 'temp' was a global variable in Logic.cpp and editor.cpp.
Even if there were, it seems like it would easily lead to some undefined
behavior. So it's good to clean this up.
This is the "Behavioral logic", "Basic Physics", and "Collisions with
walls" trio.
They were originally aligned but then I removed global args, thus
misaligning them. So now I'm re-aligning them back again.
Surprisingly not that many. It looks like at one point in development,
damage blocks were created for every single spike in the Tower, before
it was too laggy so they switched to directly checking collision with
the tile instead.
This removes a bunch of commented-out code that was clearly kept from
the Flash version, even though the Flash graphics API is much different
than SDL's. Also removes a bunch of TODOs that either say nothing, or
say something whose meaning has been totally lost to time due to being
completely vague, or something that's already been done and someone
forgot to remove the TODO.
Most of this is telecookie/quickcookie stuff, which was used in the
Flash version, but there's no longer any such thing as a save cookie.
Also one TODO that says to make a function that's now been made.
Unlike, say, the scriptclass i/j/k stuff, these tempstrings are only
purely visual, and there's no known glitches to manipulate them anyway.
Thus I think it's safe to make this cleanup.
It looks like this may have been used earlier in development, judging
from the name, obviously, but right now it seems like it's used as an
error message if a main game level is asked for an invalid room (well,
only two of them - the Lab and Warp Zone). It should probably be
formalized into an error system, if we want to keep teststring, and also
people would never see it anyway because I don't think there's a
reliable and consistent way to trigger loading a non-existent room.
I have seen someone manage to load a non-existent Warp Zone room only
one time, but even then this teststring didn't pop up. So this
teststring doesn't even trigger in the right circumstances.
Also, when it does pop up, as far as I can tell it will stay onscreen,
which is kinda annoying. So I'm just removing this ancient relic from
the code.
To be fair, it was more on the level of entire functions using different
indentation than the surrounding code, but it's not consistent enough
for me to justify leaving it alone.
Looks like this function was created because editorclass::load() takes
in a string by reference, not by value, and thus mutates it afterwards,
so if you passed a string in when you didn't want it to be mutated, bad
things would happen.
However, a better workaround for the above issue would simply to
duplicate the string and pass that string instead, thus the original
string wouldn't be affected.
To reiterate, I just want to remove the mixed indentation that just
randomly pops up in the middle of a file, because it gets annoying.
Thus, the indentation of a particular piece of code should simply match
the surrounding code. And I consider it completely fine that this file
switches from indenting 4-wide spaces to tabs starting from
Graphics::setcol() onwards. I don't think it's worth it to untabify
Graphics::setcol() and below.
This removes all indentation that suddenly switches in the middle of a
function. Most particularly egregious offenses are the ones made by the
person who has 2-wide tabs, but keeps tabbing up to make each
indentation level match up with the 4-wide spaces, so to them (and only
them) it will look just fine, but since by default tabstop is 8-wide,
their lines are pushed off all the way to the right.
This changes something like UtilityClass::String to help.String,
basically. It takes less typing this way, and is a neat effect of having
global args actually be global variables.
Now that it does nothing due to some earlier changes, it's a useless
function that does nothing. (Well, it was already a useless function,
but those earlier changes made it clearer just exactly how useless it
is.) So remove that function and remove all its callsites.
'swfStage' gets set to 'stage' in updategraphicsmode() but... that does
absolutely nothing, because they both contain exactly the same thing.
And these variables aren't referenced anywhere else. So I'm removing
both of these variables.
Although it keeps getting set to true and false in various places, it
never once gets checked, essentially deeming it a variable that's used
but does nothing.
The 'r', 'g', and 'b' arguments do absolutely nothing. Except unlike
Graphics::drawtile(), there's only one version of Graphics::drawtile2(),
so just remove those args and update callers.
The 'r', 'g', and 'b' arguments do absolutely nothing, even though
they're used in the version of Graphics::drawtile() that's more used. So
delete the other version without those extra arguments, and then remove
the extra arguments from the remaining version. And then update callers.
This removes all global args in all functions in titlerender.cpp.
Additionally, all 'dwgfx' has been renamed to 'graphics' in that file
(there are a lot of them, as you might guess).
This commit removes the passing around of global args in the logic
functions. Additionally, all 'dwgfx' has been replaced with 'graphics'
in Logic.cpp.
This removes global arg passing from all functions on editorclass.
Callers have been updated correspondingly. Additionally, all 'dwgfx' has
been replaced with 'graphics' in editor.cpp.
This commit removes all global args from the parameters of each function
on the scriptclass object, and updates all places they are called
accordingly. It also changes all instances of 'dwgfx' to 'graphics' in
Script.cpp.
Interestingly enough, it looks like editor.h depended on Script.h's
class define of the musicclass. I've temporarily placed the class define
in editor.h, but by the end of this patchset it'll be gone.
This removes global args from all functions on the Graphics class.
Callers of those functions in other files have been updated accordingly.
Of course, since Graphics.cpp is already in the Graphics namespace,
I do not need to change all 'dwgfx' to 'graphics' in Graphics.cpp.
This commit removes the global args being passed around from the
function args on the mapclass object, as well as updating all callers in
other files to not have those args. Furthermore, 'dwgfx' has been
renamed to 'graphics' in Map.cpp.
This commit removes all global args from functions on the entityclass
object, and updates the callers of those functions in other files
accordingly (most significantly, the game level files Finalclass.cpp,
Labclass.cpp, Otherlevel.cpp, Spacestation2.cpp, WarpClass.cpp, due to
them using createentity()), as well as renaming all instances of 'dwgfx'
in Entity.cpp to 'graphics'.
I've decided to call dwgfx/game/map/obj/key/help/music the "global args".
Because they're essentially global variables that are being passed
around in args.
This commit removes global args from all functions on the Game class,
and deals with updating the callsites of said functions accordingly. It
also renames all usages of 'dwgfx' in Game.cpp to 'graphics', since the
global variable is called 'graphics' now.
Interesting to note, I was removing the class defines from Game.h, but
it turns out that Graphics.h depends on the mapclass and entityclass
defines from Game.h. And also Graphics.h spelled mapclass wrong (it
forgot the "class") so I just decided to use that existing line instead.
This is only temporary and after all is said and done, at the end of
this pull request those class defines will be gone.
This is a refactor that turns the script-related arrays `ed.sb`, and
`ed.hooklist` into C++ vectors (`script.commands` was already a vector, it was
just misused). The code handling these vectors now looks more like idiomatic
C++ than sloppily-pasted pseudo-ActionScript. This removes the variables
`script.scriptlength`, `ed.sblength`, and `ed.numhooks`, too.
This reduces the amount of code needed to e.g. simply remove something from
any of these vectors. Previously the code had to manually shift the rest of
the elements down one-by-one, and doing it manually is definitely error-prone
and tedious.
But now we can just use fancy functions like `std::vector::erase()` and
`std::remove()` to do it all in one line!
Don't worry, I checked and `std::remove()` is in the C++ standard since at least
1998.
This patch makes it so the `commands` vector gets cleared when
`scriptclass::load()` is ran. Previously, the `commands` vector never actually
properly got cleared, so there could potentially be glitches that rely on the
game indexing past the bounds set by `scriptlength` but still in-bounds in the
eyes of C++, and people could potentially rely on such an exploit...
However, I checked, and I'm pretty sure that no such glitch previously existed
at all, because the only times the vector gets indexed are when `scriptlength`
is either being incremented after starting from 0 (`add()`) or when it's
underneath a `position < scriptlength` conditional.
Furthermore, I'm unaware of anyone who has actually found or used such an
exploit, and I've been in the custom level community for 6 years.
So I think it's fine.
Someone being mean could've overwritten the telesaves of unsuspecting
players, or unlocked a bunch of stuff which they shouldn't have for
those players, using things like the telesave() command and gamestates.
To prevent this, return early in Game::savequick(), Game::savetele(),
and Game::unlocknum() if we are in custommode.
Instead of passing the error codes out of the function, just handle the
errors directly as they happen, and fail gracefully if something goes
wrong instead of continuing.
Whenever you would press Alt+Enter, or Alt+F, or on macOS Command+Enter,
or on macOS Command+F, or F11, the game would print this useless error
message to console, every single time: "Error: failed: " and it would
concatenate SDL_GetError() after it, but most of the time SDL_GetError()
is blank, so it would print just that.
Instead, what the fullscreen shortcut will now do is check the result of
the relevant SDL functions, BEFORE it decides to print an error message.
And when it DOES print an error message, it will be less vague and will
say instead "Error: toggling fullscreen failed: <output of
SDL_GetError()>".
This means Screen::ResizeScreen() and Screen::toggleFullScreen() are now
int-returning functions. Ideally, every function interfacing with SDL
would return an error code, but that's too much for this simple patch.
Additionally, I took the opportunity to clean up the surrounding
formatting of the code a bit, most notably dedenting the
keypress-clearing stuff by one tab level, converting the
shortcut-handling code to spaces, and removing commented-out code.
Each check has been put in its own variable, so the final conditional is
more readable, and the ifdef is no longer right smack in the middle of
an if-statement.
Also the control flow has been changed (the "else" has been removed from
the shortcut-checking conditional) so I could put the variables closer
to the actual conditional itself. I don't think it affects anything,
though.
This patch allows the use of pressing F11 to toggle fullscreen, as well as
allowing the use of Right Command when using the Command+Enter/F shortcut on
macOS. Apparently Alt+Enter isn't the only shortcut to toggle fullscreen,
Alt+F also works, which I didn't know before.
I'm adding F11 as a shortcut because it's a far more natural shortcut to
toggle fullscreen than this Alt+Enter or Alt+F business, which seems to be a
relic mimicking some other games and some Microsoft stuff?
I'm also adding RCommand+Enter/F because I see no reason not to. If you can
use RAlt on non-macOS, why can't you use RCommand on macOS, too?
I also cleaned up the formatting relating to the shortcut code, and made sure
the closing parenthesis was outside the ifdef so my text editor wouldn't
highlight the parenthesis inside the non-macOS ifdef-branch as a dangling
closing parenthesis, because it assumes the one in the branch above is the
actual closing parenthesis and doesn't parse macros.
If you Alt+Tabbed while in fullscreen mode, the game would stay in
fullscreen instead of switching to windowed, but there was a chance it
would EITHER use the same internal resolution which would mismatch the
window resolution (don't know when exactly this happens, but still) and
stay being in an actual windowed mode, OR switch between
fullscreen/windowed every other time you re-focused the window, which is
annoying.
Now, whenever you Alt+Tab in fullscreen, the game will be in windowed
mode, and then when you re-focus it will go back to fullscreen.
Consistently.
Previously, the only way to guarantee that the game actually saved your
unlock.vvv after changing an option, was to ensure you pressed ACTION on
the "quit game" menu option.
This makes Alt+F4 graceful in that pressing it will also save
unlock.vvv, as it should. Now truly UN-graceful ways of exiting the
game, such as SIGSEGV, SIGABRT, or pkill -9 or pkill -15 will not save
unlock.vvv, as should be the case.
Text outline is not drawn on roomtext when you're actually playing the
game, so don't draw the outline in the editor, either.
FIQ mistakenly added text outline to roomtext in
ca9f577fc4.
There's an if-else chain that first deals with figuring out if there's
an entity where your left-click happened, and to do this it uses
edentat(), which returns a sentinel value of -1 if there is NOT an
entity where your cursor is.
It's very important to check that the value returned ISN'T -1 before you
start indexing the 'edentity' vector, since if you DO index it with that
-1, it'll result in Undefined Behavior because you're doing an
out-of-bounds array access.
Now, here's what the if-else chain looked like before:
if(tmp==-1 && ed.free(ed.tilex,ed.tiley)==0)
{
...
}
else if(edentity[tmp].t==1)
The bug here is very subtle but it was an easy oversight. Basically, if
'ed.free' ended up not being zero, control flow would jump to the next
"else if" over, which then ends up asking for the -1th index of
'edentity', which is Undefined Behavior.
This undefined behavior has now resulted in a crash on my system after
TerryCavanagh/VVVVVV#172, due it shuffling things around juuuuust enough
such that this UB would end up resulting in a segfault instead of
chugging along and working fine. For me and my system, this meant that
if my first left-click in the editor upon opening the game was me
placing down a tile and not placing down an entity, the game would
crash. But, it would be fine if I first placed down an entity and then
afterwards placed down tiles, because it's UB.
And I'm almost certain this was the cause of the very strange bug where
you couldn't hold down left-click for the foreground-placing tool (but
you COULD for the background-placing tool) that seemed to occur most
often on Windows (TerryCavanagh/VVVVVV#25).
The solution to this is to stick in another conditional in the tree
before any indexing occurs, such that there's no way any other
conditionals with the indexing in the conditional tree could end up
being hit. In summary, the if-else chain looks like this now:
if(tmp==-1 && ed.free(ed.tilex,ed.tiley)==0)
{
...
}
else if(tmp == -1)
{
//Important! Do nothing, or else Undefined Behavior will happen
}
else if(edentity[tmp].t==1)
This turns the array 'edentity' into a proper vector, and removes the need to
use a separate length-tracking variable and manually keep track of the actual
amount of edentities in the level by using the long-winded
'EditorData::GetInstance().numedentities'. This manual tracking was more
error-prone and much less maintainable.
editorclass::naddedentity() has been removed due to now functionally being the
same as editorclass::addedentity() (there's no more
'EditorData::GetInstance().numedentities' to not increment) and for also being
unused in the first place.
editorclass::copyedentity() has been removed because it was only used to shift
the rest of the edentities up manually, but now that we let C++ do all the
hard work it's no longer necessary.
This refactors the roomtext code to (1) not use ad-hoc objects and (2)
not use a separate length-tracking variable to keep track of the actual
amount of roomtext in a room.
What I mean by ad-hoc object is, instead of formally creating a
fully-fledged struct or class and storing one vector containing that
object, this game instead hacks together an object by storing each
attribute of an object in different vectors.
In the case of roomtext, instead of making a Roomtext object that has
attributes 'x', 'y', and 'text', the 'text' attribute of each is stored
in the vector 'roomtext', the 'x' attribute of each is stored in the
vector 'roomtextx', and the 'y' attribute of each is stored in the
vector 'roomtexty'. It's only an object in the sense that you can grab
the attributes of each roomtext by using the same index across all three
vectors.
This makes it somewhat annoying to maintain and deal with, like when I
wanted add sub-tile positions to roomtext in VVVVVV: Community Edition.
Instead of being able to add attributes to an already-existing
formalized Roomtext object, I would instead have to add two more
vectors, which is inelegant. Or I could refactor the whole system, which
is what I decided to do instead.
Furthermore, this removes the separate length-tracking variable
'roomtextnumlines', which makes the code much more easy to maintain and
deal with, as the amount of roomtext is naturally tracked by C++ instead
of us having to keep track of the actual amount of roomtext manually.
This fixes a source of undefined behavior, where the int returned by
ss_toi() would be random garbage memory if the string passed into it
would be empty. That's because if the string is empty, there are no
characters to parse, so nothing simply gets put into x.
The easiest way to pass an empty string in to ss_toi() would be to use
script commands with empty arguments.
The problem was that the code seemed to be wrongly copy-pasted from the
code for generating the trinket-found text boxes (to the point where
even the comment for the crewmate-found text boxes didn't get changed
from "//Found a trinket!").
For the trinket-found text boxes, they use y-positions 85 and 135 if not
in flip mode, and y-positions 105 and 65 if the game IS in flip mode.
These text boxes are positioned correctly in flip mode.
However, for the crewmate-found text boxes, they use y-positions 85 and
135 if not in flip mode, as usual, but they use y-positions 105 and 135
if the game IS in flip mode. Looks like someone forgot to change the
second y-position when copy-pasting code around.
Which is actually a bit funny, because I can conclude from this that it
seems like the code to position these text boxes in flip mode was
bolted-on AFTER the initial code of these text boxes was written.
I can also conclude (hot take incoming) that basically no one actually
ever tested this game in flip mode (but that was already evident, given
TerryCavanagh/VVVVVV#140, less strongly TerryCavanagh/VVVVVV#141, and
TerryCavanagh/VVVVVV#142 is another flip-mode-related bug which I guess
sorta kinda doesn't really count since text outline wasn't enabled until
2.3?).
So I fixed the second y-position to be 65, just like the y-position the
trinket text boxes use. I even took the opportunity to fix the comment
to say "//Found a crewmate!" instead of "//Found a trinket!".
Previously, when MMMMMM is installed but the user is using PPPPPP, niceplay would still restart the song even if it's the same. That has been fixed. In addition, Plenary and Path Complete no longer loop when MMMMMM is installed but PPPPPP is in use.
This fixes another way you could end up typing on a non-existent line in
the script editor.
In a script with only 1 line, which is empty, the game would let you
press backspace on it, removing the line. This results in you typing on
a non-existent line.
You will keep typing on it until you either close the script or press
Up. If you press Up, you will be unable to get back to the non-existent
line, for it doesn't exist - but the text you typed on the non-existent
line will still be there, until you close the script and re-open it.
This makes the "[Press ENTER to return to editor]" fade out after a few frames, allowing screenshots of custom levels to be cleaner and to make sure nothing is obscured while the user is editing their level.
This commit also adds alpha support in BlitSurfaceColoured, where it takes into account the alpha of the pixel *and* the alpha of the color.
`graphics::getRGBA(r,g,b,a)` was added to help with this.
Following discussion on TerryCavanagh/VVVVVV#153, I suggested that
instead of reverting my M&P guards from TerryCavanagh/VVVVVV#124 (which
would only revert it for The Final Level, The Lab, Overworld, and The
Tower, leaving Space Station 1 & 2 and The Warp Zone alone which could
potentially cause the same problem that motivated
TerryCavanagh/VVVVVV#153), we should initialize the map data with 0s
instead.
It turns out that the line `tstring=tstring[tstring.size()-1];` also appears once in Scripts.cpp.
This causes the game to segfault after activating a terminal with an empty line at the end of it.
I added a quick `if` around this line, and set `tstring` to an empty string when needed.
In `editor.cpp`, there's a few sections of code that try and index stuff using `string.length()-1`.
This causes issues where if the string is empty, the result is -1, causing undefined behavior.
Flibit fixed a few of these cases, like on line `375` of editor.cpp:
`if((int) tstring.length() - 1 >= 0) // FIXME: This is sketchy. -flibit`
It turns out that one of these weren't caught, over at line `471`.
`tstring=tstring[tstring.length()-1];`
This causes builds compiled on Windows to segfault if you load more than one level in the editor.
I added a quick `if` around it, setting `tstring` to an empty string, which seems to fix the problem.
It's really obvious that screen shaking is not processed in towers if
you bring up the pause menu then quickly quicksave and bring it back
down. The screen won't shake, but it will suddenly start shaking if you
exit the tower, finishing off the stalled screenshake timer.
If you died in (11,7) and a moving platform was to the left of the line
x=152, even if it was moving vertically it would get snapped to x=152,
in custom levels.
Surprised nobody has ran into this before (although people have ran into
the other kludge, which is placing tile 59 at [18,9] if you're in a room
on either the line x=11 or y=7).
Previously, in tower mode, being inside walls would just kill you, unlike
being inside walls outside tower mode, which was somewhat confusing.
Also, spikes behaved differently with regards to invincibility, being
unsolid in towers but solid outside them.
This does not change the behaviour of the "edge" spikes in towers.
The only places where you can die in a room without a room name is the
Overworld, and I feel that calling it Dimension VVVVVV is appropriate.
You can't naturally die in The Ship nor the Secret Lab, and you can only
do it by pressing R, so I didn't feel it appropriate to add checks to
make the hardest room be "The Ship" or "The Secret Lab" if you managed
to get your hardest room to be a room in either of those areas.
If you looked at the "- Press ENTER to Teleport -" text in flip mode,
you might have noticed that the outline looked pretty strange and
glitched-out for some reason. It's just the outline rendering
upside-down. To fix this, make PrintOff() use flipbfont instead of
bfont.
The problem with flipme() was that it WAS properly reflecting
("reflecting" as in mirroring over a given line) the text box over the
line y=120, BUT it forgot to account for the height of the text box.
Thus, the text box position would be off by the length of its own
height. And when the text box got taller, this offset would worsen and
worsen.
In flip mode, warping entities would appear to change color for a brief
moment. This is actually them defaulting to the actual color used in
flipsprites.png, instead of first being whited-out and then re-colored
using graphics.setcol().
To fix this, use BlitSurfaceColoured() and pass `ct` along instead of
using BlitSurfaceStandard().
This is useful for distributions, which may not want to put data.zip in
the same directory as the binary. This can't be distribution-specific
due to the license ("Altered source/binary versions must be plainly
marked as such, and must not be misrepresented as being the original
software.").
If you died in Prize for the Reckless, which is at (11,7), and respawned
in the same room, tile 59 (a solid invisible tile) would be placed at
[18,9] to prevent the moving platform from going back through the
quicksand.
Unfortunately, the way that this kludge was added is poor.
First, the conditional makes it so that it doesn't happen in ONLY
(11,7). Instead of being behind a positive conditional, the tile is
placed in the else-branch of an if-conditional that checks for the
normal case, i.e. if the current room is NOT (11,7), thus being a
negative conditional.
In other words, the positive conditional is "game.roomx == 111 &&
game.roomy == 107". To negate it, all you would have to do is
"!(game.roomx == 111 && game.roomy == 107)".
However, whoever wrote this decided to go one step further, and actually
DISTRIBUTE the negative into both statements. This would be fine, except
if they actually got it right. You see, according to De Morgan's laws,
when you distribute a negative across multiple statements you not only
have to negate the statements themselves, but you have to negate all the
CONJUNCTIONS, too. In other words, you have to change all "and"s into
"or"s and all "or"s into "and"s.
Instead of making the conditional "game.roomx != 111 || game.roomy !=
107", the person who wrote this forgot to replace the "and" with an
"or". Thus, it is "game.roomx != 111 && game.roomy != 107" instead. As a
result, if we re-negate this and take a look at the positive
conditional, i.e. the conditional that results in the else-branch
executing, it turns out to be "game.roomx == 111 || game.roomy == 107".
This ends up forming a cross-shape of rooms where this kludge happens.
As long as your room is either on the line x=11 or on the line y=7, this
kludge will execute.
You can see this if you go to Boldly To Go, since it is (11,13), which
is on the line x=11. Checkpoint in that room, then touch a disappearing
platform, wait for it to fully disappear, then die. Then an invisible
tile will be placed to the left of the spikes on the ceiling.
Anyway, to fix this, it's simple. Just change the "and" in the negative
conditional to an "or".
The second problem was that this kludge was happening in custom levels.
So I've added a map.custommode check to it. I made sure not to make the
same mistake originally made, i.e. I made sure to use an "or" instead of
an "and". Thus, when you re-negate the negative conditional and turn it
into the positive conditional, it reads: "game.roomx == 111 &&
game.roomy == 107 && !map.custommode".
(19,8) is hardcoded to warp on all-sides no matter what. This is fine,
except for the fact that it was doing this in custom levels, too, even
despite the fact that the warp background and color would be overridden
anyway. The only workaround was to add a warp line to the room in custom
levels. I've added a check for custommode so that this won't happen.
The game uses magic values x=-500 and y=-500 to indicate when a text box
should be centered horizontally or vertically. It does this for x=-1
too, but it's buggy because it only looks at the first line of the text
box to center it. In this commit I fix it so that it will look at all of
the lines of the text box to center it instead.
This uses utfcpp combined with a custom font, in the form of a PNG and text file. By default, the game acts exactly as it did before; custom fonts can be provided by third parties.
This fixes a bug where warp lines created through createentity wouldn't
work without there already being a warp line present in the room as an
edentity.
This fixes a bug where if warpdir() was used during in-editor
playtesting, the changed warp direction would persist even when leaving
playtesting.
This would be very annoying to correct back every time you playtested
and warpdir() was used, so I've added some kludge to store the actual
warp direction of each room when entering playtesting, and then set the
warp directions back when leaving playtesting.
This commit makes `help`, `graphics`, `music`, `game`, `key`, `map`, and
`obj` essentially static global objects that can be used everywhere.
This is useful in case we ever need to add a new function in the future,
so we don't have to bother with passing a new argument in which means we
have to pass a new argument in to the function that calls that function
which means having to pass a new argument into the function that calls
THAT function, etc. which is a real headache when working on fan mods of
the source code.
Note that this changes NONE of the existing function signatures, it
merely just makes those variables accessible everywhere in the same way
`script` and `ed` are.
Also note that some classes had to be initialized after the filesystem
was initialized, but C++ would keep initializing them before the
filesystem got initialized, because I *had* to put them at the top of
`main.cpp`, or else they wouldn't be global variables.
The only way to work around this was to use entityclass's initialization
style (which I'm pretty sure entityclass of all things doesn't need to
be initialized this way), where you actually initialize the class in an
`init()` function, and so then you do `graphics.init()` after the
filesystem initialization, AFTER doing `Graphics graphics` up at the
top.
I've had to do this for `graphics` (but only because its child
GraphicsResources `grphx` needs to be initialized this way), `music`,
and `game`. I don't think this will affect anything. Other than that,
`help`, `key`, and `map` are still using the C++-intended method of
having ClassName::ClassName() functions.
This has two benefits:
(1) The game uses less resources when it is asked to gotoroom to the
same room because it is no longer redrawing the warp background
every single frame, which is very wasteful.
(2) The warp background no longer freezes or flickers if the player is
standing inside a gotoroom script box (which calls gotoroom every
frame or every other frame, because every time the gotoroom happens
the script box gets reloaded).
First, two bug fixes. Room text input mode wasn't properly unset
upon pressing Esc, making the prompt get stuck, requiring you to
add roomtext again and finish it to make it go away. Secondly,
escaping script text input would remove the wrong entity.
I also tweaked the handling slightly so that instead of deleting
the entity if it already existed if escaping from text input,
it merely reverts the change in script name/roomtext to what it
was previously.
I considered refactoring the editor text input handler entirely,
but figured such a change would be a bit too extensive for the
purpose of this repository.
Ingame entities are drawn backwards, probably to draw the player on top,
being entity 0 (usually, at least). Make the level editor draw entities
in the same order.
This is to make sure that the room data of those areas are not
distributed with the M&P binary, even if they are already inaccessible
because of other M&P ifdefs (I tested).
When the game enter towermode, it adjusts player amd camera x/y depending
on what screen the player entered it from (The Tower) or the loadlevel
mode ("minitowers"; Panic Room and The Final Challenge). This code didn't
account for respawning to checkpoints. This is unlikely to matter in most
circumstances, but can cause problems in some corner cases, or with R abuse.
This could cause a player to die, respawn outside camera edges and then
immediately die again due to the edge spikes, repositioning the camera
properly. Invincibility would cause further issues, but that's Invincibility
Mode for you -- if this was the only problem I wouldn't bother.
I added a check that repositions the tower camera appropriately if a player
enter a tower as part of the respawn process.
FillRect() is similar enough to memset when blending isn't used, so the
game will take a fast path drawing the roomname background when the
background is opaque.
This is the variable dwgfx.translucentroomname and <translucentroomname>
in unlock.vvv.
This lets you see through the black background of the roomname at the
bottom of the screen, i.e. it makes the roomname background translucent.
So you can see if someone decides to hide pesky spikes there.
The roomname background used to just be a simple SDL_Rect that was drawn
using SDL_FillRect with a color of 0. Unfortunately, it seems that you
cannot use transparent colors with SDL_FillRect, it just defaults to
being fully opaque. However, you CAN draw surfaces with translucency,
which seems like the easiest thing to do. But the first step is to
convert the roomname background to an SDL_Surface.
This replaces the FillRect()s with SDL_BlitSurface() in the three places
roomnames are drawn: in towerrender, in gamerender, and in editorrender.
For some reason, there are two lines that have been copy-pasted the
exact same way and in the exact same place, namely being at the end of
each branch of the if-else conditional, which makes them be executed no
matter what. If they're going to be executed no matter what, we might as
well make it clearer and take those two lines out of each branch.
I ran the game through Valgrind to catch various issues.
One thing caught my attention -- map.resumedelay. This is
used by The Tower/etc to add a delay before the game is
resumed (probably for camera controls delaying it) for
spawning the player. This variable was uninitialized. Notably,
if I explicitly set it to 592851 or similar, it reproduces the
bug, even if I cannot reproduce it with my typical compilation
flags. So fixing this by initializing it to 0, alongside some
other Valgrind warnings, should fix the problem entirely.
* Add a null terminator to loaded TinyXML files
The TinyXML parse() function expect a C-like string, including terminator.
When xml loading was changed, it loaded the file, but included no such thing.
Thus, we load the file, then reallocate the memory so that we can insert a
null terminator to it, before passing it to parse().
* Tweak TinyXML file loading
Instead of first loading the file content into memory, then reallocate it
to add a null pointer, add an argument to the file load function for whether
to append a null terminator or not, defaulting to false. It still returns the
length without the null pointer in case a length ptr is passed.
An earlier change caused TinyXml to prettyprint specifically the
contents of entities (script names and roomtext) a bit more than
before in level files, and added an unusually high amount of whitespace
(particularly, it added an empty line to every entity). This happens
because, for some reason, an empty string is explicitly being added
when creating an entity XML element. The line is so mysterious it feels
like it probably somehow solved a nasty bug long ago and shouldn't be
touched, but it was probably just a mistake, and with all that
whitespace it doesn't look good.
Previously, if it was called in a custom level, it would say "out of
Twenty" no matter what, even if the level had zero trinkets. This commit
fixes that.
Previously, it was a hardcoded list going up to fifty.
This time, it's less hardcoded. Most of the time, it will take a number,
find out its tens-place word, find out its ones-place word, and combine
the two together. There are special cases for the teens, and the numbers
zero through nine, and one hundred.
Also, now if it is given a negative value, it will just return "???"
instead.
If there are more than one hundred it will still say "Lots".
2.2 will handle this just fine, because there are 100 slots allocated
for trinkets and crewmates, and it will save and load all 100 slots just
fine. Except, it only resets the first 20 slots when starting a level
from the beginning, but that's minor and already fixed in 2.3 anyway.
This commit makes it so you can now place up to one hundred trinkets and
crewmates in the editor.
When editorclass::reset() was resetting the contents of the level
previously, it was mixing up the X and Y bounds. The Y bound was
supposed to be 30*maxheight, and the X bound was supposed to be
40*maxwidth. Instead, it took 30*maxwidth as its Y bound and
40*maxheight as its X bound.
Then, when it actually indexes the contents vector to set each tile to
0, it used 30*maxwidth instead of 40*maxwidth.
The difference between width and height is a bit hard to spot, but one
thing you can do to remember the difference is to remember the fact that
X corresponds with width, and Y corresponds with height. Also, rooms are
40 by 30 tiles, and so X (and therefore width) should correspond with
40, and Y (and therefore height) should correspond with 30.
As a result of mixing up the variables, whenever you played a 20x20 map,
quit the level and then started making a new 20x20 map, the tiles of the
last four rows of the previous map would persist, from y=16 (1-indexed)
all the way to y=20 (1-indexed).
I don't recall anyone ever running into this bug before, which is a bit
strange. But if no one truly has ever ran into this bug before, then I'm
genuinely surprised.
While working on the patch to fix the enemy type room property of each
room not getting reset, and testing the fix, I noticed that for some
reason some contents of the previous level I played in order to test the
enemy type property persisting was ALSO persisting alongside the enemy
type property.
Then I read the code and when I realized that the X and Y bounds were
getting mixed up I groaned. Very loudly.
This fixes a bug where if you loaded a level, then started making a new
level in the editor, the enemy types from the previous level would
persist.
While working on VVVVVV: Community Edition and adding a new room
property for enemy speed, I noticed that enemy type was not getting
reset at all. After some testing, I confirmed that this was the case. So
this bug is fixed now.
This fixes a bug where you could get mismatched text and background
colors on the menu screen by being in the Tower and exiting at a certain
time. The background when mismatched will always be red, which seems to
have something to do with the fact that when you enter the Tower the
game always sets the background to be red. I could get a quick repro
setup going by starting in the checkpoint in Teleporter Divot, then
quickly entering the Tower, exiting, then re-entering, then pressing Esc
and quitting - all done very quickly.
The important thing about this mismatch is that it's only temporary, and
will be corrected when you press ACTION and the color of the text and
background in the menu both change at the same time, which is what
map.nexttowercolour() does. So all I do is simply call that function
when exiting to the menu, and it will fix the color mismatch.
Earlier in 53950e14de65a54d9369c5183a16337782d3dc4e, I made playing a
song while a song was already fading quickly fade out the current song,
but then added an exception for if the fade came from the musicfadeout()
command. This commit adds another exception for if the fade came from
niceplay(), since otherwise the music transitions between areas in the
game would go too quick.
This fixes an issue where you could softlock the game if you exited
playtesting mode by pressing Esc while a "- Press ACTION to advance text
-" prompt was onscreen, then re-entered playtesting and then started any
script.
The "- Press ACTION to advance text -" prompt is most directly
controlled by game.advancetext, but when it appears game.pausescript is
usually set as well. You need to have both variables on at the same time
in order to press ACTION. If only advancetext is on, then you won't be
able to flip but can still move around, but if only pausescript is on,
then the game softlocks because the only way you can turn pausescript
off is by pressing ACTION, but the game will only let you press ACTION
if *both* advancetext and pausescript are on.
This fixes a bug where bringing up the pause screen while in a tower
would draw the room you entered BEFORE the tower during the time it took
for the pause screen to be brought up or down.
This fixes a bug in the editor where if you had cutscene bars active
while exiting playtesting, when you re-entered playtesting, the bars
would do their closing animation even though they should be already
gone.
Out-of-bounds array access is Undefined Behavior, which means Bad
Things.
In this particular case, it was indexing an array by using the
`testeditor` variable. Which is fine, except it was indexing that array
*in a conditional that only happens if `testeditor` is -1*. So it was
indexing an array at position -1, which is Out of Bounds and is Not
Good.
For a long time, VVVVVV has had an issue where if you used custom
graphics with translucent pixels (i.e. pixels whose opacity was neither
0% nor 100%, but somewhere between 0% and 100%), those pixels would end
up looking very ugly. They seemed to be overlaid on top of some weird
blue color, instead of actually being translucent.
This bug only occurred when in-game and not in towermode. So, most of
the time, basically. The translucent pixels only displayed properly
inside the Tower, both minitowers, and the editor (when not
playtesting).
I traced this down to the use of the magic number 0xDEADBEEF in
Graphics::drawmap() and Graphics::drawfinalmap(). Looks like someone had
fun finding a color. Was it you, simoroth? Anyway, I've changed it so it
simply uses 0 instead. Note that this magic number needs to be changed
for BOTH FillRect() and OverlaySurfaceKeyed(), or else the game's
background will be some random solid color instead of actually being
drawn properly.
There's a long-standing issue where the Direct Mode status of the loaded
custom map in memory never resets properly. So if you load a level with
a certain layout of Direct Mode rooms, that same layout will be
preserved if you start making a new level in the editor. This commit
fixes that issue.
Similar to 2ebccbc3e9, there's also a
long-standing bug where if you backspace an empty line (and this time,
the line IS actually already empty, not merely
emptied-earlier-in-the-frame), the game will quickly delete more blank
lines if there are any above the blank line you deleted. Again, this is
annoying too, if you so happen to need to use lots of blank lines.
To fix this, it's simple - just set ed.keydelay to 6 when the game
backspaces an empty line. Then it won't be so trigger-happy in deleting
blank lines.
There is a long-standing bug with the script editor where if you delete
the last character of a line, it IMMEDIATELY deletes the line you're on,
and then moves your cursor back to the previous line. This is annoying,
to say the least.
The reason for this is that, in the sequence of events that happens in
one frame (known as frame ordering), the code that backspaces one
character from the line when you press Backspace is ran BEFORE the code
to remove an empty line if you backspace it is ran. The former is
located in key.Poll(), and the latter is located in editorinput().
Thus, when you press Backspace, the game first runs key.Poll(), sees
that you've pressed Backspace, and dutifully removes the last character
from a line. The line is now empty. Then, when the game gets around to
the "Are you pressing Backspace on an empty line?" check in
editorinput(), it thinks that you're pressing Backspace on an empty
line, and then does the usual line-removing stuff.
And actually, when it does the check in editorinput(), it ACTUALLY asks
"Are you pressing Backspace on THIS frame and was the line empty LAST
frame?" because it's checking against its own copy of the input buffer,
before copying the input buffer to its own local copy. So the problem
only happens if you press and hold Backspace for more than 1 frame.
It's a small consolation prize for this annoyance, getting to
tap-tap-tap Backspace in the hopes that you only press it for 1 frame,
while in the middle of something more important to do like, oh I don't
know, writing a script.
So there are two potential solutions here:
(1) Just change the frame ordering around.
This is risky to say the least, because I'm not sure what behavior
depends on exactly which frame order. It's not like it's key.Poll()
and then IMMEDIATELY afterwards editorinput() is run, it's more
like key.Poll(), some things that obviously depend on key.Poll()
running before them, and THEN editorinput(). Also, editorinput() is
only one possible thing that could be ran afterwards, on the next
frame we could be running something else entirely instead.
(2) Add a kludge variable to signal when the line is ALREADY empty so
the game doesn't re-check the already-empty line and conclude that
you're already immediately backspacing an empty line.
I went with (2) for this commit, and I've added the kludge variable
key.linealreadyemptykludge.
However, that by itself isn't enough to fix it. It only adds about a
frame or so of delay before the game goes right back to saying "Oh,
you're ALREADY somehow pressing backspace again? I'll just delete this
line real quick" and the behavior is basically the same as before,
except now you have to hit Backspace for TWO frames or less instead of
one in order to not have it happen.
What we need is to have a delay set as well, when the game deletes the
last line of a char. So I set ed.keydelay to 6 as well if editorinput()
sses that key.linealreadyemptykludge is on.
For a long time, the script editor has had a bug where it would let you
put the cursor on a nonexistent script line, which would APPEAR to be
the last line of the script... but in reality, it WASN'T the last line
of the script, and in fact, the ACTUAL last line was the line ABOVE the
script.
So, if you typed anything on this nonexistent line, it would appear to
get erased when exiting the script. Thus, people have (erroneously)
misdiagnosed this as the script editor somehow being trigger-happy and
erasing lines when it shouldn't be, when in reality it should've have
let you gone onto that line in the first place!
Hiding the mouse cursor does makes sense in a lot of situations I agree, but it's very much a preference thing, and I don't see a good reason to change the original behavour. Some people (i.e. me) don't like cursors disappearing in windowed mode when you move the cursor over the screen, but most importantly, it makes the editor much less pleasant to use (since you're relying on the 30fps editor cursor box).
I'd be happy to support, say, a setting that allowed you to disable the mouse cursor, or even a 15 second time-out which makes the cursor disappear if not moved (and reappear once moved), but just having it off by default feels wrong to me.
Yikes. Somebody brought this to my attention, I didn't even remember that I'd written it. "Spa" or "Spastic" is kind of a south park esque slang term that used to be pretty common in Ireland, which I used without even thinking about it. It's definitely not something I would say anymore, 10 years on, and it's something I shouldn't have said at the time either. I'm sorry :(
(somebody on twitter was asking me about how much cleaning up of the source code I did before launching this. I think this commit kinda answers that)
* Add entity type attribute checks to getcrewman()
This means that the game is no longer able to target other non-crewmate
entities when it looks for a crewmate.
This has actually happened in practice. A command like
position(cyan,above) could position the text box above a horizontal warp
line instead of the intended crewmate.
This is because getcrewman() previously only checked the rule attributes
of entities, and if their rule happened to be 6 or 7. Usually this
corresponds with crewmates being unflipped or flipped, respectively.
But warp lines' rules overlap with rules 6 and 7. If a warp line is
vertical, its rule is 5, and if it is horizontal, its rule is 7.
Now, usually, this wouldn't be an issue, but getcrewman() does a color
check as well. And for cyan, that is color 0. However, if an entity
doesn't use a color, it just defaults to color 0. So, getcrewman() found
an entity with a rule of 7 and a color of 0. This must mean it's a cyan
crewmate! But, well, it's actually just a horizontal warp line.
This commit prevents the above from happening.
If you're in the room being targeted when a `warpdir()` command is run,
and it sets the warp direction to none (warp dir 0), and if you're in a
Lab or Warp Zone room, the background will be set to the wrong one,
because it will always set the background to the stars-going-left
background, instead of setting it to the Lab background or the
stars-going-up background.
However, this is only a visual glitch, and is a temporary one, because
if you manage to trigger a `gotoroom()` on the room, it will get set to
its proper background.
This commit makes it so if you're in a Lab room, the background gets set
to the Lab background, and if you're in a Warp Zone room, the background
gets set to the stars-going-up background.
There are three map-related vectors that need to be reset in hardreset:
`map.roomdeaths`, `map.roomdeathsfinal`, and `map.explored`. All three
of these vectors use the concatenated rows system, whereby each room is
given a room number, calculated by doing X + Y*20, and this becomes
their index in each vector.
There's a double-nested for-loop to handle resetting all of these, where
the outer for-loop iterates over the Y-axis and the inner for-loop
iterates over the X-axis, but only `map.explored` is properly reset.
That's because it's the only vector that properly indexes using X +
Y*20. The other vectors reset using X, so previously, only the first row
of `map.roomdeaths` and `map.roomdeathsfinal` got reset, corresponding
with only the first row of rooms in both Dimension VVVVVV and Outside
Dimension VVVVVV getting reset.
This commit makes sure that both get reset properly.
There are two main problems with flipgravity():
1. It doesn't work for an already-flipped crewmate.
2. It doesn't work on the player.
This commit addresses both of those issues.
For reference, here is the list of scores for a custom level:
0 - not played
1 - finished
2 - all trinkets
3 - finished, all trinkets
Previously, you could've played a level and finished it and set a score
of 3. Then you could re-play the entire level, but manage to only get a
score of 1, i.e. only finished but not with all trinkets. Then your
score for that level would get set back to 1, which basically nullifies
the time you spent playing that level and getting all of the trinkets.
This commit makes it so your new score will get set only if it is higher
than the previous one.
This disables being able to press M to mute while editing a script in
the script editor. It also disables it in the script list, too, but I'm
hoping no one thinks that's an issue, because I personally don't think
it is.
This fixes a bug where if you died after activating a teleporter prompt
and then respawned in the same room as the teleporter, the teleporter
prompt would go away. Which would be very annoying if you wanted to, you
know, teleport.
You don't even have to R-press to make this happen. "Level Complete!"
and Energize are two teleporter rooms in the main game that have hazards
in them.
I see no reason to set game.activetele to false when dying. For one, it
gets set to false during a gotorom anyway. For another, I couldn't get
the game to activate the teleport prompt in a room without a teleporter
in my testing, mostly because when you touch a teleporter, your
checkpoint is set to that teleporter, so you can't R-press to go to
another room and carry over the teleport prompt.
This uncomments the code for the font outline. Now, you will see the
font outline on text that had it in the Flash version, most notably "-
Press ENTER to Teleport -" and the time trial overlay text.
This also fixes a problem with the commented code, where un-centered
text didn't outline properly.
The game makes sure that the player entity is never destroyed, but in
doing so, it doesn't destroy any duplicate player entities that might
have been created via strange means e.g. a custom level doing a
createentity with t=0.
Duplicate player entities are, in a sense, not the "real" player entity.
For one, they can take damage and die, but when they do they'll still be
stuck inside the hazard, which can result in a softlock. For another,
their position isn't updated when going between rooms. It's better to
just destroy them when we can.
Both projects can distribute the game's original assets, provided they compile with the makeandplay define set, and do not distribute the original levels.
This is similar to what was reverted in 3011911 but this time should use
syntax available even before C++11, tested on Clang with
`-std=c++98 -Wall -pedantic`.
I've never given out my real name online, so I'll sort my username as a
last name. My personal preference would have been to sort the list by
first name instead of last name, since that'd be clearer, plus you
often recognize people by their first name rather than their last name.
The TinyXml functions to load and save files don't properly support
unicode file paths on Windows, so in order to support that properly, I
saw no other option than to do the actual loading and saving via PHYSFS
(or to use the Windows API on Windows and retain doc.LoadFile and
doc.SaveFile on other OSes, but that'd be more complicated and
unnecessary, we already have PHYSFS, right?).
There are two new functions in FileSystemUtils:
bool FILESYSTEM_saveTiXmlDocument(const char *name, TiXmlDocument *doc)
bool FILESYSTEM_loadTiXmlDocument(const char *name, TiXmlDocument *doc)
Any instances of doc.SaveFile(<FULL_PATH>) have been replaced by
FILESYSTEM_saveTiXmlDocument(<VVVVVV_FOLDER_PATH>, &doc), where
<FULL_PATH> included the full path to the saves or levels directory,
and <VVVVVV_FOLDER_PATH> only includes the path relative to the VVVVVV
directory.
When loading a document, a TiXmlDocument used to be created with a full
path in its constructor and doc.LoadFile() would then be called, now a
TiXmlDocument is constructed with no path name and
FILESYSTEM_loadTiXmlDocument(<VVVVVV_FOLDER_PATH>, &doc) is called.
There's now a thin layer of UTF-16 around the WinAPI functions to get
the path to the Documents folder and to create a new directory, so that
account usernames with non-ASCII characters do not result in no VVVVVV
folder being created or used.
Resizing the vector does the same thing that the loops did, it changes
the size for the vector and initializes it with default-constructed
elements (or 0 or its equivalent for POD types).
Where a specific value is needed, it is set with the second
parameter of resize().
The next official VVVVVV build removes 32-bit Linux (like all my other games),
and I need to get rid of the shell script on macOS at some point, so this
basically brings it up to what my other games are doing. Plus, this probably
fixes a bug where someone tries to run their executable away from the root...
Doing this is not necessary as CMake already looks up the default one
correctly and in fact breaks whenever you have multiple Xcode versions
installed so Xcode is not called Xcode.app.
(And it does not respect the default Xcode set using xcode-select)
The behavior now is to respect CMAKE_OSX_SYSROOT if set by the
user on the command line. If it's not set, try to use the hardcoded
path to the 10.9 SDK if it's present. If not, warn about the fact
that a different SDK is used.
- Return in Unlock Play Modes goes to Game Options instead of the main menu
- changing some Graphics Options settings no longer plays two menu select sounds, which was really loud :(
- Return in Game Pad Options plays the menu select sound
- turning off Screen Effects plays the menu select sound
When building on macOS targeting an older version than the version of
the SDK currently used, this prevents accidentally using APIs that are
too new (introduced in macOS versions newer than the deployment target).
Follow-up to #19 which did the change for macOS.
It appears to work as expected on Linux too.
Tested on a distro where XDG_DATA_HOME is undefined by default,
and `PHYSFS_getPrefDir` also resolves to `~/.local/share/VVVVVV/`.
The first organization parameter is unused on Linux and macOS.
Instead use PHYSFS_getPrefDir which does the same than the manual
concatenation done before. The organization name argument is required
but is not used on macOS.
key:${{ runner.os }}-brew-${{ hashFiles('/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/ninja.rb', '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/sdl2.rb') }}# Using hash of formula files if available, or a fixed key for simplicity if not easily determined
- name:Install dependencies
if:steps.cache-brew.outputs.cache-hit != 'true'
run:brew install ninja sdl2
- name:CMake configure (default version)
run:|
mkdir -p ${SRC_DIR_PATH}/build && cd ${SRC_DIR_PATH}/build
VVVVVV's source code is made available under a custom license. Basically, you can compile yourself a copy, for free, for personal use. But if you want to distribute a compiled version of the game, you might need permission first. See the [License exceptions](License%20exceptions.md) page for more information.
VVVVVV Source Code License v1.0
-------
Last updated on January 7th, 2020.
This repo contains the source code for VVVVVV, including all level content and text from the game. It does not, however, contain any of the icons, art, graphics or music for the game, which are still under a proprietary license. For personal use, you can find these assets for free in the [Make and Play Edition](http://distractionware.com/blog/category/vvvvvv-make-and-play/).
This repo contains the source code for VVVVVV, including all level content and text from the game. It does not, however, contain any of the icons, art, graphics or music for the game, which are still under a proprietary license. For personal use, you can find these assets for free in the [Make and Play Edition](https://thelettervsixtim.es/makeandplay/).
If you are interested in distributing work that falls outside the terms in the licence below, or if you are interested in distributing work that includes using any part of VVVVVV not included in this repo then please get in touch - we can discuss granting a licence for that on a case by case basis. The purpose of making the contents of this repo available is for others to learn from, to inspire new work, and to allow the creation of new tools and modifications for VVVVVV.
VVVVVV's source code is made available under a [custom license](LICENSE.md), which states that you must not distribute any materials from the game (i.e. the game's assets) which are not included in this repo unless approved by us in writing. In general, if you're interested in creating something that falls outside the license terms, get in touch with [Terry](http://distractionware.com/email/)!
There is an important general exception: if you're compiling a [Make
and Play](https://thelettervsixtim.es/makeandplay/) version of the game, then you can you freely distribute the game's assets.
The following is a list of projects which have been given permission by Terry to distribute the assets with distributions of the game, and under what conditions.
Exceptions granted to the VVVVVV source code license
| Any distribution of the VVVVVV: Make and Play edition | | All distributions and packages of the Make and Play edition can freely distribute the data assets, so long as they compile with the makeandplay define and do not distribute the original levels.| Must compile with the makeandplay define set, cannot distribute the original levels. | |
| VVVVVV: Make and Play Edition |[Terry Cavanagh](http://distractionware.com/)|The free and official version of VVVVVV that includes player levels, and the tools to create your own levels, but does not include the original levels from the game.| Must compile with the makeandplay define set, cannot distribute the original levels. | [download](https://thelettervsixtim.es/makeandplay/) |
| Ved | [Dav999 and InfoTeddy](https://github.com/Daaaav/Ved/graphs/contributors) | An external editor for VVVVVV levels. | No conditions. | [download](https://tolp.nl/ved/), [github repo](https://github.com/Daaaav/Ved) |
| VVVVVV: Community Edition | https://github.com/v6cord/VVVVVV-CE/graphs/contributors | Community fork of VVVVVV focused on expanding the capabilities of player levels. | Must compile with the makeandplay define set, cannot distribute the original levels. | [github repo](https://github.com/v6cord/VVVVVV-CE) |
| VVVVVVwasm|kotborealis|Web Assembly port of VVVVVV| Must compile with the makeandplay define set, cannot distribute the original levels. | [github repo](https://github.com/kotborealis/VVVVVVwasm) |
| cursed_vvvvvv.exe | [MustardBucket](https://twitter.com/mustard_bucket/) | Modified version of VVVVVV where instead of flipping gravity you jump normally, can jump multiple times, and wall jump. | Make it impossible to revert to ordinary flipping behaviour. | [download](https://mustardbucket.itch.io/cursed-vvvvvv?secret=O0KvS02wD473pXBF9avreZsww), [twitter gif](https://twitter.com/mustard_bucket/status/1216272971779670016) |
| Haiku Port | [Julius C. Enriquez](https://github.com/win8linux) | Port for the Haiku operating system. | Display the following text in the Haiku package to make it clear that this is an exception: "VVVVVV is a commercial game! The author has given special permission to make this Haiku version available for free. If you enjoy the game, please consider purchasing a copy at [thelettervsixtim.es](http://thelettervsixtim.es)." | [haiku recipe](https://github.com/haikuports/haikuports/tree/master/games-arcade/vvvvvv), [haiku data.zip recipe](https://github.com/haikuports/haikuports/tree/master/games-arcade/vvvvvv_data) |
| Dreamcast Port | [Gustavo Aranda](https://github.com/gusarba/) | Port for the Sega Dreamcast. | Permission is given to distribute a ready-to-use CD image file for the Sega Dreamcast containing the data.zip assets for non commercial use only. | [github repo](https://github.com/gusarba/VVVVVVDC)|
| XBox One/UWP Port | [tunip3](https://github.com/tunip3) | Port for XBOX ONE (DURANGO) via UWP. | Permission is given to distribute a pre-compiled package (containing the data.zip assets) for people to run on development mode xboxes, for non commercial use only. | [github repo](https://github.com/tunip3/DURANGO-V6)|
| armhf Port | [johnnyonFlame](https://github.com/johnnyonFlame/) | Armhf port for Raspberry PI and other SBC devices| Permission is for non commercial use only. Display the following text in the readme to make it clear that this is an exception: "VVVVVV is a commercial game! The author has given special permission to make this port available for free. If you enjoy the game, please consider purchasing a copy at [thelettervsixtim.es](http://thelettervsixtim.es)."| [github release](https://github.com/JohnnyonFlame/VVVVVV/releases/tag/v2.4-r1) |
| PortMaster distributions of the game for Linux Handheld devices | [portmaster](https://portmaster.games/) | A port manager GUI for Linux handheld devices | Permission is for non commercial use only. Display the following text in the readme to make it clear that this is an exception: "VVVVVV is a commercial game! The author has given special permission to make this port available for free. If you enjoy the game, please consider purchasing a copy at [thelettervsixtim.es](http://thelettervsixtim.es)."| [website](https://portmaster.games/detail.html?name=vvvvvv) |
| Wii Port | [Alberto Mardegan](https://github.com/mardy/) | Port for the Nintendo Wii. | Permission is given to distribute a ready-to-use build for the Nintendo Wii containing the data.zip assets for non commercial use only. | [github repo](https://github.com/mardy/VVVVVV/tree/wii) |
| Recalbox Port | [digitalLumberjack](https://gitlab.com/recalbox/recalbox) | Port for Recalbox project. | Display the following text in the readme to make it clear that this is an exception: "VVVVVV is a commercial game! The author has given special permission to make this port available for free. If you enjoy the game, please consider purchasing a copy at [thelettervsixtim.es](http://thelettervsixtim.es)." | [website](https://recalbox.com/) |
VVVVVV's source code is made available under a custom license. See [LICENSE.md](LICENSE.md) for more details.
This is the source code to VVVVVV, the 2010 indie game by [Terry Cavanagh](http://distractionware.com/), with music by [Magnus Pålsson](http://souleye.madtracker.net/). You can read the [announcement](http://distractionware.com/blog/2020/01/vvvvvv-is-now-open-source/) of the source code release on Terry's blog!
In general, if you're interested in creating something that falls outside the license terms, get in touch with Terry and we'll talk about it!
The source code for the desktop version is in [this folder](desktop_version).
Authors
VVVVVV is still commercially available at [thelettervsixtim.es](https://thelettervsixtim.es/) if you'd like to support it, but you are completely free to compile the game for your own personal use. If you're interested in distributing a compiled version of the game, see [LICENSE.md](LICENSE.md) for more information.
Discussion about VVVVVV updates mainly happens on the "unofficial" [VVVVVV discord](https://discord.gg/Zf7Nzea), in the `vvvvvv-code` channel.
Credits
-------
- Created by [Terry Cavanagh](http://distractionware.com/)
- Room Names by [Bennett Foddy](http://www.foddy.net)
- Music by [Magnus Pålsson](http://souleye.madtracker.net/)
- Metal Soundtrack by [FamilyJules](http://familyjules7x.com/)
- Music by [Magnus Pålsson](https://magnuspalsson.com/)
- Metal Soundtrack by [FamilyJules](https://link.space/@familyjules)
- 2.0 Update (C++ Port) by [Simon Roth](http://www.machinestudios.co.uk)
- 2.2 Update (SDL2/PhysicsFS/Steamworks port) by [Ethan Lee](http://www.flibitijibibo.com/)
- Additional coding by [Misa Kai](https://infoteddy.info/)
- Beta Testing by Sam Kaplan and Pauli Kohberger
- Ending Picture by Pauli Kohberger
Versions
------------
There are two versions of the VVVVVV source code available - the [desktop version](https://github.com/TerryCavanagh/VVVVVV/tree/master/desktop_version) (based on the C++ port, and currently live on Steam), and the [mobile version](https://github.com/TerryCavanagh/VVVVVV/tree/master/mobile_version) (based on a fork of the original flash source code, and currently live on iOS and Android).
- Localisations by [our localisation teams](desktop_version/TRANSLATORS.txt)
- With additional contributions by [many others here on github](desktop_version/CONTRIBUTORS.txt) <3
option(ENABLE_WERROR"Treat compilation warnings as errors"OFF)
option(BUNDLE_DEPENDENCIES"Use bundled TinyXML-2, PhysicsFS, and FAudio (if disabled, TinyXML-2, PhysicsFS, and FAudio will be dynamically linked; LodePNG, C-HashMap and SheenBidi will still be statically linked)"ON)
option(STEAM"Use the Steam API"OFF)
option(GOG"Use the GOG API"OFF)
option(OFFICIAL_BUILD"Compile an official build of the game"OFF)
option(MAKEANDPLAY"Compile a version of the game without the main campaign (provided for convenience; consider modifying MakeAndPlay.h instead"OFF)
if(OFFICIAL_BUILDANDNOTMAKEANDPLAY)
set(STEAMON)
set(GOGON)
endif()
option(REMOVE_ABSOLUTE_PATHS"If supported by the compiler, replace all absolute paths to source directories compiled into the binary (if any) with relative paths"ON)
If you need a font (like a TTF) converted into the format that the game can read, for now you might want to ask Dav, who has tools for it.
=== F O N T F O R M A T ===
Fonts consist of two files: a .png and a .fontmeta. The .png contains all the "images" for all glyphs, and the .fontmeta is an XML document containing all information about which characters are in the file and other metadata.
For example, a font for Japanese might be called font_ja.png and font_ja.fontmeta.
The fontmeta file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<font_metadata>
<display_name>日本語</display_name>
<width>12</width>
<height>12</height>
<white_teeth>1</white_teeth>
<chars>
<range start="0x20" end="0x7F"/>
<range start="0xA0" end="0x17F"/>
<range start="0x18F" end="0x18F"/>
<range start="0x218" end="0x21B"/>
<range start="0x259" end="0x25A"/>
<!-- ... -->
</chars>
<special>
<range start="0x00" end="0x1F" advance="6"/>
<range start="0xEB00" end="0xEBFF" color="1"/>
</special>
<fallback>buttons_12x12</fallback>
</font_metadata>
* type: not specified for normal fonts. <type>buttons</type> is used in button glyph fonts.
* display_name: the name of the language the font is specifically meant for - in the language itself. Users will see this in the level editor when choosing a font to use. If this font is used equally by multiple translations, this could be set to a combination like "繁體中文/한국어". (If you are creating a custom player level: don't worry about this)
* width/height: the width and height of each glyph in the font. Every character will always be drawn as a rectangle of this size. VVVVVV is rated to support fonts up to 12 pixels high - anything higher may cause text overlapping or not fitting in place.
* white_teeth: indicates that all characters in the font are white, so the game itself doesn't have to remove all color from the image and make all pixels white like it would in old versions of the game. If this is not set to 1, this font cannot have colored (button) glyphs, and the game has to specifically process the font every time it is loaded, so 1 is highly recommended.
* chars: defines which characters are in the image. Starting at the top left of the image, each character is simply a rectangle of equal size (defined in <width> and <height>) from left to right, top to bottom. In the example given above, the image first has Unicode every character from U+0020 up to and including U+007F, then every character from U+00A0 to U+017F, and so on. To include a single character, simply use a range with equal start and end attributes equal.
* special: defines special attributes that will be applied to a range of characters. One or more of the following attributes can be used:
- color: set to 1 if these glyphs should be drawn with its original colors (for button glyphs, or even emoji...)
- advance: instead of <width>, the cursor (for drawing the next character) should be advanced this amount of pixels to the right. This controls the width of a character, but it does not affect how characters are arranged in the image, and the full glyph will still be drawn. While this means the font system has support for variable-width fonts, it's recommended to not use this option. There are some problems arising from using a variable-width font (especially in text boxes), so please consider copying the font's fullwidth forms (U+FF01-U+FF5E) to ASCII U+0021-U+007E instead. One may argue that a monospaced font also fits more with the game's style.
* fallback: specifies the button glyphs font to use. Make sure to choose one that fits fully within your [width]x[height] rectangle, so for an 8x12 font, choose buttons_8x8, not buttons_12x12.
This file will explain what you need to know when maintaining translations of VVVVVV (like adding new strings to the game and syncing them across languages).
For making new translations of the game, read README-translators.txt instead.
=== A D D I N G N E W S T R I N G S ===
If you want to add some new text to the game, all you generally need to do to make it translatable is wrap loc::gettext() around your raw strings in the code (you may need to add an #include "Localization.h"), and add the same strings to the English language file. The new strings can be automatically synced from English to all other language files using the translator menu.
For example, "Game paused" can be made translatable by changing it to loc::gettext("Game paused"). Its entry in the English language file could look like this:
The max value indicates how many characters of space there is for the text, and is further described below. It looks like "40" for single-line text, and "38*5" for multi-line text. The max value may not be applicable or may be hard to define, so this attribute can be completely left out. For example, when it's a diagonally-placed menu option, or because the limit depends on the lengths of other strings (like Low/Medium/High in the joystick menu), or a string looks progressively worse the longer it gets. As a general rule: if defining a hard limit would be misleading, then it can be exempt from having a limit.
=== E D I T I N G E X I S T I N G S T R I N G S ===
Sometimes you need to make a partial change to text that has already been translated.
For example: you need to change the string "Press ENTER to stop" to "Press {button} to stop" (because you made that hotkey configurable).
Please do *not* simply find-and-replace the text in the language files, nor remove the old string when you add the new one. (Whether you keep the translations or not.)
Instead, duplicate the string in the English language file, putting your version underneath the old version, and mark the old version with the explanation "***OUTDATED***".
For example:
<string english="Press ENTER to stop" translation="" explanation="stop super gravitron"/>
↓ ↓ ↓
<string english="Press ENTER to stop" translation="" explanation="***OUTDATED***"/>
<string english="Press {button} to stop" translation="" explanation="stop super gravitron"/>
The game won't be using the outdated string anymore - it's only still in the language files to carry the existing translations for reference. Once a translator updates the language files, they can reuse parts of the old translation for the new version of the string.
font::print_wrap(PR_CEN, -1, 50, "Hello world, this will wordwrap to the screen width", 255, 255, 255);
A not-technically-exhaustive list of all flags (which are defined in Font.h):
- PR_2X/PR_3X/.../PR_8X Print at larger scale (PR_1X is default)
- PR_FONT_INTERFACE [DEFAULT] Use interface (VVVVVV language) font
- PR_FONT_LEVEL Use level-specific font (room names, cutscenes, etc)
- PR_FONT_8X8 Use 8x8 font no matter what
- PR_BRIGHTNESS(value) Use this brightness 0-255 (this value is mixed with
r, g and b for an alpha effect, and accounts for
colored glyphs correctly)
- PR_BOR Draw a black border around the text
- PR_LEFT [DEFAULT] Left-align text/place at X coordinate
- PR_CEN Center-align text relative to X (X is center)
or to screen if X == -1
- PR_RIGHT Right-align text to X
(X is now the right border, not left border)
- PR_CJK_CEN [DEFAULT] Large fonts (Chinese/Japanese/Korean) should
stick out on top and bottom compared to 8x8 font
- PR_CJK_LOW Larger fonts should stick out fully on the bottom
(draw at Y)
- PR_CJK_HIGH Larger fonts should stick out fully on the top
- PR_RTL_XFLIP In RTL languages, mirror the X axis, so left is 320
and right is 0, and invert the meaning of PR_LEFT and
PR_RIGHT
=== S T R I N G F O R M A T T I N G ===
Instead of sprintf-family functions, it is preferred to use VVVVVV's own formatting system, VFormat.
Strings sometimes have placeholders, which look like {name} or {name|flags}. For example, "{n_trinkets} of {max_trinkets}".
Placeholders can also have "flags" that modify their behavior. These can be added or removed in the translation as needed. Flags are separated by | (pipe).
For more info, see the documentation at the top of VFormat.h.
Full example:
char buffer[100];
vformat_buf(buffer, sizeof(buffer),
"{crewmate} got {number} out of {total} trinkets in {m}:{s|digits=2}.{ms|digits=3}",
=> "Vermilion got 2 out of 20 trinkets in 2:03.001"
=== T R A N S L A T O R M E N U ===
The translator menu has options for both translators and maintainers - it allows testing menus, translating room names within the game, syncing all language files with the English template files, getting statistics on translation progress, and more.
VVVVVV will show a "translator" menu in the main menu if either:
- The "lang" folder is NOT next to data.zip, and the game is running somewhere within a "desktop_version" folder, and desktop_version/lang IS found. This normally happens when compiling the game from source;
- The command line argument (or launch option) "-translator" is passed.
- ALWAYS_SHOW_TRANSLATOR_MENU is defined during compilation (see top of Localization.h)
To add new strings, add them to only the English strings.xml or strings_plural.xml, and use the option to sync all languages from the translator menu. This will copy the new strings to all translated language files.
The language file sync option has differing support for the language files. As indicated in the menu itself, it handles each file as follows:
[Full syncing EN→All]
For these files, the English version of the file is fully copied and overwrites every language's version, while all existing translations and customizations are inserted for every language. This means newly added strings are copied to every language, and removed strings are simultaneously removed from every language, bringing them fully up-to-date.
- meta.xml
- strings.xml
- strings_plural.xml
- cutscenes.xml
- roomnames.xml
- roomnames_special.xml
[Syncing not supported]
These files are untouched by the syncing feature.
- numbers.xml
=== F I L E S ===
* meta.xml: This file contains some general information about a translation.
* strings.xml: This file contains general strings for the interface and some parts of the game.
* strings_plural.xml: Similar usage to strings.xml, but for strings that need different plural forms ("1 trinket", "2 trinkets")
* numbers.xml: This file contains all numbers from 0 to 100 written out (Zero, One, etc). This file also allows you to define the plural forms used in strings_plural.xml.
* cutscenes.xml: This file contains nearly all the cutscenes that appear in the main game.
* roomnames.xml: This file contains nearly all the room names for the main game.
* roomnames_special.xml: This file contains some special cases for roomnames, some names for rooms that usually aren't displayed as regular (like The Ship), and some general area names.
This file will explain everything you need to know when making translations of VVVVVV.
WARNING TO VOLUNTEERS: VVVVVV translation is not a community effort where anyone can submit translations! It's *possible* that, on a case-by-case basis, someone can volunteer to become an official translator. But you would need to get approval from Terry first (or you can express interest in there being an official translation into your language, it may already be planned!)
Likewise, you're welcome to report issues in existing translations (or to submit PRs to fix these issues), but it's not a good idea to rewrite significant parts of a translation, and then contribute it without warning. If you think there are errors or things that could be improved, please give an explanation as to why. (We may decide to discuss it with the official translator.)
It *is* possible to make a fan translation for fun, for sharing with others, etc. But it probably won't be distributed with the game officially.
=== A D D I N G A N E W L A N G U A G E ===
The English language files are basically templates for other languages (all the translations are empty).
To create a new language, simply copy the `en` folder, and start by filling out meta.xml (further explained below).
Alternatively, you can create an empty language folder, and then use the in-game sync tool (translator > maintenance > sync language files) to populate it.
=== E X C E L ===
The game uses XML files for storing the translations. If you prefer, there is an .xlsm file which can be used as an editor. This can load in all the XML files, and then save changes back as XML.
If you're an official translator, you should have received a version of this spreadsheet. If not, a blank version can be found here: https://github.com/Daaaav/TranslationEditor
=== T R A N S L A T O R M E N U ===
The translator menu has options for both translators and maintainers - it allows testing menus, translating room names within the game, syncing all language files with the English template files, getting statistics on translation progress, and more. The translator menu is hidden from players in regular versions of the game.
When the translator menu is unlocked, you can also press F8 anywhere in the game to reload the current language files. So you can save translations and immediately preview them (except for menu buttons and the current cutscene dialogue, which can't be reloaded on the fly). You will hear a coin sound when the language files have been reloaded via F8.
=== L O W E R C A S E A N D U P P E R C A S E ===
If lowercase and uppercase does not exist in your language (Chinese, Japanese and Korean for example), you can set toupper to 0 in meta.xml, and ignore any directions about using lowercase or uppercase.
VVVVVV's menu system has the style of using lowercase for unselected options and uppercase for selected options, for example:
play
levels
[ OPTIONS ]
translator
credits
quit
The menu options are stored as their full-lowercase version, and they're normally commented as "menu option" in the translation files. A built-in function (toupper in Localization.cpp) automatically converts the lowercase text to uppercase when needed. This function has support for a good number of accented characters, Cyrillic and Greek, but more could be added if needed. It also accounts for special cases in Turkish and Irish.
Turkish: The uppercase of i is İ, for example, "dil" becomes "DİL" and not "DIL". To enable this, set toupper_i_dot to 1 in meta.xml.
Irish: Specific letters may be kept in lowercase when making a string full-caps. For example, "mac tíre na hainnise" should be "MAC TÍRE NA hAINNISE" instead of "MAC TÍRE NA HAINNISE". If enabled, you can use the ~ character before the letter which should be forced in lowercase: "mac tíre na ~hainnise". This ~ character only has an effect in strings which are subject to automatic uppercasing (otherwise it'll be visible as å). This can be enabled by setting toupper_lower_escape_char to 1 in meta.xml.
=== W O R D W R A P P I N G A N D L E N G T H L I M I T S ===
For most languages, VVVVVV can automatically wordwrap based on spaces. This may not work for some languages (like Chinese, Japanese and Korean), so instead, newlines can be inserted manually (see below) and automatic wordwrapping can be disabled in meta.xml.
VVVVVV's resolution is 320x240, and the default font is 8x8, which means there is a 40x30 character grid (although we don't adhere to this grid for the UI, but it gives a good indication). Naturally, if the font has a different size like 12x12, less characters will fit on the screen too.
Strings are usually annotated with their limits (for example, max="38*3"). This can be formatted like one of the following:
(A) 33
(B) 33*3
(A) if it's a single number (for example "33"): the hard maximum number of characters that are known to fit. Being exactly on the limit may not look good, so try to go at least a character under it if possible.
(B) if X*Y (for example 33*3): the text should fit within an area of X characters wide and Y lines high. The text is automatically word-wrapped to fit (unless disabled in meta.xml). If automatic word-wrapping is disabled, you need to manually insert newlines with |, or possibly as a literal newline.
If your language uses a font with a different size than 8x8, there will be two limits given: `max`, which is the original limit based on the 8x8 font, and `max_local`, which is adapted to the size of your font. To get this notation, use the maintenance option to sync language files from within VVVVVV. Ensure the correct font is set in meta.xml first.
The translator menu has an option ("limits check") to automatically find strings that break the given limits. There may be a few cases where this detection isn't perfect, but it should be a helpful quality assurance tool.
The maximum lengths are not always given. Notoriously, menu option buttons are placed diagonally, thus they have maximums that are hard to look up. Even more so, making an option differ too much in length from the other options might make it look out of place. Best thing to do there is probably just translate as usual and then test all menus via the "menu test" option in the translator menu. However, menus do automatically reposition based on the text length, so worst-case scenario, if an option is 36 characters long, all options are displayed right underneath each other.
=== F O N T S ===
The game uses an 8x8 pixel font by default (font.png and font.fontmeta in the "fonts" folder). If your language can be represented in 8x8 characters, it is preferable to use this font, or for this font to be extended.
The fonts directory also has a README.txt file that explains how the font format works.
=== N U M B E R S A N D P L U R A L F O R M S ===
In certain places, VVVVVV (perhaps unconventionally) writes out numbers as full words. For example:
- One out of Fifteen
- Two crewmates remaining
- Two remaining
These words can be found in numbers.xml. The numbers Zero through Twenty will be the most commonly seen. It's always possible for numbers up to One Hundred to be seen though (players can put up to 100 trinkets and crewmates in a custom level).
Your language may not allow the same word to be used for the same number in different scenarios. For example, in Polish, "twenty out of twenty" may be "dwadzieścia z dwudziestu". Right now, you have two sets of wordy numbers to choose from, `translation` and `translation2`, but this will likely change to a more customizable system in the future. You can choose when these "wordy" numbers are used and when numeric forms (20 out of 20) are used (see "STRING FORMATTING" below). It's also possible to leave the translations for all the numbers empty. In that case, numeric forms will always be used.
In English, using Title Case is appropriate, but in most other languages, it probably isn't. Therefore, you may want to translate all numbers in lowercase, when it's more appropriate to use "twenty out of twenty" than "Twenty out of Twenty". You can then apply auto-uppercasing to any placeholder you choose (see "STRING FORMATTING" below), making it possible to display "Twenty out of twenty".
As for plural forms: English and some other languages have a singular (1 crewmate) and a plural (2 crewmates). Some languages may have different rules (like for 0, or numbers that end in 2, 3 and 4). VVVVVV can accommodate these rules and allows you to translate certain strings (strings_plural.xml) in different ways depending on the number. The different forms can be defined by changing the "form" attribute on each number in numbers.xml. For English, form "1" is used for singular, and form "0" is used for plural. You can set up any amount of plural forms you will need.
Numbers that identify the forms do not need to be sequential, you may use any number between 0 and 254 to identify the different forms. So instead of using forms 0, 1, 2 and 3, you could also name them 1, 2, 5 and 7.
Suppose you need a different form for the number 1, the numbers 2-4, and all other numbers. You could use "form 1" for the number 1, "form 2" for 2-4, and "form 0" for all other numbers:
<numbers>
<number value="0" form="0" ... />
<number value="1" form="1" ... />
<number value="2" form="2" ... />
<number value="3" form="2" ... />
<number value="4" form="2" ... />
<number value="5" form="0" ... />
<number value="6" form="0" ... />
...
When translating the plural strings, you can add translations for every unique form. For example:
Plural forms can appear both for wordy numbers ("you saved one crewmate") as well as numbery numbers ("you died 136 times in this room"), so we need the plural forms to go further than 100.
For the numbers 100 and higher: as far as I can find (with information about plural rules across 160 languages) - the plural forms always repeat themselves every 100 numbers. So numbers 100-199 always have the same forms as 200-299, 300-399, and so on. However, 100-119 (200-219, etc) don't always work the same as 0-19 do (in English for example, it's not "101 trinket" despite ending in 01). Therefore, forms for 100-119 can also be filled in. The system will simply copy 20-99 for 120-199, and that should be enough to cover all numbers from 0 to infinity. Technically the system supports providing forms until 199, but it should never be necessary to go higher than 119, so they're not in the language files by default.
Numbers higher than 100 cannot have a written out translation ("one hundred and one" does not exist).
=== S T R I N G F O R M A T T I N G ===
Strings sometimes have placeholders, which look like {name} or {name|flags}. For example, "{n_trinkets} of {max_trinkets}".
Placeholders can also have "flags" that modify their behavior. These can be added or removed in the translation as needed. Flags are separated by | (pipe).
For example, "{n_trinkets|wordy}" makes the number of trinkets display as a "wordy" number (twenty instead of 20) (See "NUMBERS AND PLURAL FORMS"). "{n_trinkets|wordy|upper}" makes that word start with a capital letter (Twenty instead of twenty). So for example, "{n_trinkets|wordy|upper} of {max_trinkets|wordy}" may be displayed as "Twenty out of twenty" - assuming numbers.xml is translated all-lowercase.
The valid flags are:
- wordy [ints only] use number words (Twenty) of the first set (translation), instead of digits (20)
- wordy2 [ints only] use number words of the second set (translation2), instead of digits
- digits=n [ints only] force minimum n digits, like n=5 --> 00031
- spaces [only if using digits=n] use leading spaces instead of 0s
- upper uppercase the first character with loc::toupper_ch
=== S T O R Y A N D C H A R A C T E R I N F O R M A T I O N ===
This is a brief story and character overview for reference. Any further questions can be directed to Terry: https://distractionware.com/email/
== The Crewmates ==
VVVVVV is about a crew of curious and super-intelligent aliens exploring the universe. There are six crewmates onboard the ship, in six different colours. They are:
* Viridian (Cyan, the player character)
* Verdigris (Green)
* Vitellary (Yellow)
* Vermilion (Red)
* Victoria (Blue)
* Violet (Purple)
All six characters have names that start with V. In addition, each crewmate's name is an obscure word that suggests their colour - for example, Verdigris is the name of the green pigment that forms on copper when it oxidises. This might be hard to translate! (So, potentially it just makes sense not to translate the names at all, unless you have a good idea about how to do it - for example, if you can pull off the same colour/name beginning with V trick in your language.)
Each crewmate has the following "rank", i.e. their job on the ship. These are all basically just Star Trek inspired roles:
* "Captain" Viridian (Captain as in leader)
* "Chief" Verdigris (Chief as in Lead Engineer)
* "Professor" Vitellary (Kind of the senior scientist on board)
* "Officer" Vermilion (Officer as in Away-Officer, the one who usually goes out exploring)
* "Doctor" Victoria (Doctor in the scientific sense)
* "Doctor" Violet (Doctor in the medical sense)
Verdigris, Vitellary and Vermilion are male. Victoria and Violet are female. Viridian is deliberately unspecified - when translating cutscenes, if at all possible, try to avoid specifying their gender, even if that leads to some otherwise awkward phrasing or requires the use of some cutting edge grammar tricks in your language.
== Personalities ==
If it helps with tone: the running joke in VVVVVV's writing is that all six characters are hyper intelligent prodigies, who will nevertheless speak to each other as if they're small children. E.g. Viridian "we were just playing with it!", Vermilion "I'm helping!", Verdigris "I'm an engineer!", etc. More specifically:
* Viridian is a classic hero - unphased by danger, never worried, always completely certain that they'll be able to fix everything.
* Verdigris is a romantic - he has a huge (reciprocated!) crush on Violet, which he does a terrible job of keeping secret.
* Vitellary is an academic - he loves science, and has a dry and long winded science thing to tell you in almost every cutscene.
* Vermilion is bold and adventurous - after you rescue him, you'll find him exploring different parts of the world on his own quest to find the rest of the crew (he's not much help, though).
* Victoria is a worrier - she's quick to feelings of despair. Victoria's sprite is almost always sad!
* Violet is a caretaker - she's the ship's Doctor, and most of her cutscenes are status updates about the other crewmates.
== Dimension VVVVVV ==
The world you're exploring is filled with terminals, with text logs from the previous inhabitants, who we never see. We don't know much about them.
The ship you're all on is called the "D.S.S. Souleye", which is a minor easter egg. D.S.S. just stands for "Dimensional Space Ship" - a craft that warps between different dimensions. Souleye is the pseudonym for Magnus Pålsson, the game's composer.
=== S P R I T E S T R A N S L A T I O N ===
There are several enemies in the game which contain words: STOP, YES, OBEY, LIES and TRUTH. These, as well as the C on checkpoints, can be translated.
This may be a bit tricky - the sizes of the translated graphics should be as close to the original English as possible, which means that even though letters can be compressed a bit, they will quickly be too long. For example, if you'd like to translate LIES with 5 letters, it really doesn't help if one of those 5 letters is not an I (the most slim letter).
Fortunately, the translation does not have to be literal, as the words themselves are only referenced in room names at most, and the exact meanings aren't that important, only the spirit of them is. OBEY is a They Live reference, so there are a lot of other signs you could use. Some inspiration:
* STOP: this is on a stop sign, so may not need to be translated
* OBEY: maybe something like FOLLOW, or one of the other words in "They Live" (WORK, BUY, CONSUME, CONFORM, ...)
* LIES: maybe singular LIE, maybe some form of NONSENSE (as in BS but non-offensive), FALSE, FAKE, BLAH, FABLE
* TRUTH: maybe FACTS
== Implementation of translated sprites ==
If you'd like to pixel translated sprites yourself: Take sprites.png and flipsprites.png from the graphics folder in data.zip. These spritesheets are a 32x32 grid - for example, you cannot extend OBEY upwards or to the left.
The translated file does not have to contain any untranslated sprites - these can simply be made transparent, for optimization. So: the files should be transparent sheets with some translations in the middle.
The translated versions of sprites.png and flipsprites.png can be placed in a "graphics" subfolder of your language folder.
Then, put a file next to them called "spritesmask.xml", with the following contents:
<?xml version="1.0" encoding="UTF-8"?>
<spritesmask sprite_w="32" sprite_h="32">
<sprite x="8" y="1" w="2"/> <!-- Checkpoints -->
<sprite x="4" y="2" w="4"/> <!-- STOP -->
<sprite x="4" y="3" w="4"/> <!-- YES -->
<sprite x="3" y="4"/> <!-- OBEY -->
<sprite x="2" y="5" w="2"/> <!-- LIES receiver and LIES -->
<sprite x="4" y="5" w="2"/> <!-- TRUTH -->
</spritesmask>
This file defines which sprites in the translated files should overwrite the original ones. Remove any lines for sprites you did not translate. After that, your translated sprites should work!
For completeness: sprite_w and sprite_h on the <spritesmask> tag define the size of each unit in the attributes of <sprite>. The possible attributes of <sprite> are:
* x, y: the position of the sprite to copy (in blocks of sprite_w by sprite_h)
* w, h: the width and height of the sprite to copy (default 1 for both)
* dx, dy: the destination position of the sprite (default x,y; so the same position as copied from)
=== F I L E S ===
== meta.xml ==
This file contains some general information about this translation. It contains the following attributes:
* active: If 0, this language will not be shown in the languages menu
* nativename: The name of the language in itself, fully in lowercase (so not "spanish" or "Español", but "español"). A language name can be at most 16 characters wide (in the 8x8 font)
* credit: You can fill in credit here that will appear on the language screen, like "Spanish translation by X". May be in your language. Max 38*2 @8x8
* action_hint: This is displayed at the bottom of the language screen when your language is highlighted, to show that Space/Z/V sets the selected option as the language. Max 40 @8x8
* gamepad_hint: The same as action_hint, but now a gamepad button will be filled into {button}, such as (A), (X), etc
* autowordwrap: Whether automatic wordwrapping is enabled. Can be disabled for CJK (in which case newlines have to be inserted manually in text)
* toupper: Whether to enable automatic uppercasing of menu options (unselected, SELECTED). May be disabled for languages such as CJK that don't have lowercase and uppercase.
* toupper_i_dot: When automatically uppercasing, map i to İ, as in Turkish.
* toupper_lower_escape_char: When automatically uppercasing, allow ~ to be used to stop the next letter from being uppercased, for Irish.
* rtl: This should be enabled for languages that are written right-to-left. It will horizontally flip many parts of the interface and allows text to be right-aligned in places like textboxes.
* menu_select: The indication that a certain menu option or button is selected, in addition to the automatic uppercasing if "toupper" is enabled. For example, "[ {label} ]" looks like "[ SELECTED ]"
* menu_select_tight: Similar to menu_select, except used in cases where space is a bit more limited (like the map screen). "[{label}]" looks like "[SELECTED]"
* font: The filename of the font to use. The 8x8 font is called "font", the Japanese font is "font_ja", et cetera.
== strings.xml ==
This file contains general strings for the interface and some parts of the game. In the XML, the tag for one string looks like this:
If the translation is left blank, the English text will be used as a fallback. Translations should NOT be left blank on purpose if the text is the same however; the string will be counted as untranslated and it'll be harder to keep track of what's new. Always just copy-paste the English string into the translation in that case.
The following attributes may be found for each string:
* english: the English text.
* translation: the translation.
* case: a number (1, 2, etc) for separate strings that are identical in English but may need different translations.
* explanation: an explanation about the context, location and possibly the formatting.
* max: length restrictions, described above in "WORDWRAPPING AND LENGTH LIMITS"
== strings_plural.xml ==
See "NUMBERS AND PLURAL FORMS" above.
You can define the plural forms in numbers.xml.
Then, simply add translations for each form you set up in numbers.xml. For example:
<translation form="0" translation="Shows up for all numbers with form=0"/>
<translation form="1" translation="Shows up for all numbers with form=1"/>
<translation form="2" translation="Shows up for all numbers with form=2"/>
The "wordy" flag indicates a word will be filled in (like twelve), otherwise a number (12). As described above in "STRING FORMATTING", you can change this as needed in your translations.
The `var` attribute indicates which placeholder will be filled in, the `expect` attribute indicates how high the values are that you may expect to be filled in. For example, expect="20" means any value above 20 will probably not be used in this string. This is mainly needed so that the limits check knows not to worry about a number like "seventy seven" making the string too long, but it may also be a useful context clue.
== numbers.xml ==
This file contains all numbers from 0 to 100 written out (Zero, One, etc).
This will be filled in strings like:
- One out of Fifteen
- Two crewmates remaining
- Two remaining
If this can't work for your language, or wordy numbers are really unfitting, you can leave all of these empty, in which case numbers will be used (20 out of 20).
You may want to do it all-lowercase in order to not get English-style title casing. "Twenty out of Twenty" may be grammatically incorrect in MANY languages, and "twenty out of twenty" would be better. Translating the numbers all-lowercase allows you to apply context-specific uppercasing, like "Twenty out of twenty" (see "STRING FORMATTING" above)
This file also allows you to define the plural forms used in strings_plural.xml.
For more information, see "NUMBERS AND PLURAL FORMS" above.
== cutscenes.xml ==
This file contains nearly all the cutscenes that appear in the main game. Each line has a "speaker" attribute, which is not used by the game - it's just for translators to know who says what and to establish context.
The dialogues are automatically text-wrapped, except if automatic wrapping is disabled in meta.xml. In that case, the maximum line length is 36 8x8 characters (288 pixels) or 24 12x12 characters.
In the few cases where the same text appears multiple times in a cutscene, these have the attribute "case" added to them (for example case="1", case="2", etc), so they can be translated separately if needed. (These match up with textcase(x) commands in the scripts themselves)
You may find some additional formatting attributes on each <dialogue> tag. These are used to make spacing and formatting in translations consistent with the original English text (for example, centered text, padding on both sides, etc). You can change any of these if you need, and you can also add them yourself to ANY dialogue tag.
* tt: teletype. if "1", disable automatic word wrapping, even if autowordwrap is enabled in meta.xml. You will have to add newlines manually for this textbox, either with hard enters, or with |
* wraplimit: change the maximum width of the text before it wordwraps, in pixels. Only if tt is not enabled. Example:
If autowordwrap is disabled in meta.xml, this also doesn't work, but it does give you an advisory maximum text width.
The default is 288 (36*8 or 24*12).
* centertext: center the text (but keep it aligned to the grid), for example:
[You have rescued] --[centertext="1"]--> [You have rescued]
[a crewmember!] [ a crewmember! ]
* pad: pad each line of text with a number of spaces (0 by default), for example:
[You have rescued] --[pad="2"]--> [ You have rescued ]
[ a crewmember! ] [ a crewmember! ]
This will automatically make the wrap limit smaller accordingly, unless a custom wraplimit is given.
* pad_left/pad_right: same as pad, but only affects the left or right side. For example:
[You have rescued] --[ pad_left="5"]--\ [ You have rescued ]
[ a crewmember! ] --[pad_right="2"]--/ [ a crewmember! ]
* padtowidth: pad the text on both sides if it's not this many pixels wide. For example:
[-= Personal Log =-] --[padtowidth="224"]--> [ -= Personal Log =- ] (224=28*8)
== roomnames.xml ==
This file contains nearly all the room names for the main game. The limit is always 40 8x8 characters (320 pixels) or 26 12x12 characters.
It's recommended to translate the room names in-game to see why all rooms are called what they are. To do this, enable room name translation mode in translator > translator options > translate room names.
== roomnames_special.xml ==
This file contains some special cases for roomnames, some names for rooms that usually aren't displayed as regular (like The Ship), and some general area names.
One room ("Prize for the Reckless") is intentionally missing spikes in a time trial and no death mode so the player does not have to die there, and the room is called differently in both cases (for time trial "Imagine Spikes There, if You Like", and for no death mode "I Can't Believe You Got This Far").
There are also some roomnames in the game which gradually transform into others or cycle through a few minor variations.
<dialoguespeaker="purple"english="Something has gone horribly wrong with the ship's teleporter!"translation="حصل خلل رهيب في آلة تنقيل السفينة!"/>
<dialoguespeaker="purple"english="I think everyone has been teleported away randomly! They could be anywhere!"translation="أظن أنه بعثر أفراد طاقمنا إلى أماكن عشوائية! لا أستغرب أي مكان أرسلوا إليه!"/>
<dialoguespeaker="purple"english="I'm on the ship - it's damaged badly, but it's still intact!"translation="أنا قرب السفينة. صحيح أنها تضررت كثيرا، لكنها لا تزال صالحة!"/>
<dialoguespeaker="purple"english="Where are you, Captain?"translation="أين أنت يا قبطان؟"/>
<dialoguespeaker="cyan"english="I'm on some sort of space station... It seems pretty modern..."translation="أنا قرب محطة فضائية ما... تبدو لي حديثة بعض الشيء..."/>
<dialoguespeaker="purple"english="There seems to be some sort of interference in this dimension..."translation="يبدو أن في هذا البعد نوعا ما من التشويش..."/>
<dialoguespeaker="purple"english="I'm broadcasting the coordinates of the ship to you now."translation="سأرسل إليك إحداثيات السفينة."/>
<dialoguespeaker="purple"english="I can't teleport you back, but..."translation="لا يمكنني أن أجلبك بآلة التنقيل، لكن على الأقل..."/>
<dialoguespeaker="purple"english="If YOU can find a teleporter anywhere nearby, you should be able to teleport back to me!"translation="لو عثرت بنفسك على آلة تنقيل قريبة منك، يمكنك التنقل والعودة إلي!"/>
<dialoguespeaker="cyan"english="Ok! I'll try to find one!"translation="حاضر! سأحاول أن أجد أحد هذه الأجهزة!"/>
<dialoguespeaker="purple"english="Good luck, Captain!"translation="حظا موفقا، يا قبطان!"/>
<dialoguespeaker="purple"english="I'll keep trying to find the rest of the crew..."translation="أما أنا، سأواصل البحث عن بقية أفراد الطاقم..."/>
</cutscene>
<cutsceneid="trenchwarfare"explanation="player finds Trench Warfare trinket, if no trinkets found yet">
<dialoguespeaker="cyan"english="Ohh! I wonder what that is?"translation="أوووه! يا ترى ما هذا الشيء اللماع؟"/>
<dialoguespeaker="cyan"english="I probably don't really need it, but it might be nice to take it back to the ship to study..."translation="لا أظنني بحاجة إليه، لكن لا بأس في العودة به للسفينة كي نتفحصه هناك..."/>
</cutscene>
<cutsceneid="newtrenchwarfare"explanation="player finds Trench Warfare trinket, if other trinket already found">
<dialoguespeaker="cyan"english="Oh! It's another one of those shiny things!"translation="أوه! شيء آخر من نفس نوع تلك الأشياء اللماعة!"/>
<dialoguespeaker="cyan"english="I probably don't really need it, but it might be nice to take it back to the ship to study..."translation="لا أظنني بحاجة إليه، لكن لا بأس في العودة به للسفينة كي نتفحصه هناك..."/>
<dialoguespeaker="player"english="So, Doctor - have you any idea what caused the crash?"translation="أخبريني يا دكتورة - هل عندك فكرة عن سبب الحادث؟"/>
<dialoguespeaker="purple"english="There's some sort of bizarre signal here that's interfering with our equipment..."translation="توجد هنا إشارة غريبة تتداخل مع سير عمل تجهيزاتنا..."/>
<dialoguespeaker="purple"english="It caused the ship to lose its quantum position, collapsing us into this dimension!"translation="تسبب ذلك بضياع الإحداثية الكمية للسفينة، فوقع تسطيحنا إلى هذا البعد!"/>
<dialoguespeaker="purple"english="But I think we should be able to fix the ship and get out of here..."translation="مع هذا لن نعجز عن إصلاح السفينة والمغادرة..."/>
<dialoguespeaker="purple"english="... as long as we can find the rest of the crew."translation="... بشرط أن نجمع شمل بقية الطاقم."/>
<dialoguespeaker="purple"english="We really don't know anything about this place..."translation="لا نعرف عن هذا المكان شيئا..."/>
<dialoguespeaker="purple"english="Our friends could be anywhere - they could be lost, or in danger!"translation="من يدري أين أصدقاؤنا - هل هم تائهون؟ هل يتعرضون للأخطار؟"/>
<dialoguespeaker="player"english="Can they teleport back here?"translation="هل يمكنهم التنقيل والعودة إلينا؟"/>
<dialoguespeaker="purple"english="Not unless they find some way to communicate with us!"translation="كلا، طالما لم يتواصلوا معنا بطريقة ما!"/>
<dialoguespeaker="purple"english="We can't pick up their signal and they can't teleport here unless they know where the ship is..."translation="لم نلتقط إشارتهم، ولن يتنقلوا هنا إلا لو عرفوا مكان السفينة..."/>
<dialoguespeaker="player"english="So what do we do?"translation="هكذا إذن، لكن ما الحل؟"/>
<dialoguespeaker="purple"english="We need to find them! Head out into the dimension and look for anywhere they might have ended up..."translation="أن نجدهم! لو غامرت في هذا البعد الجديد وبحثت في كل مكان ربما نلقاهم..."/>
<dialoguespeaker="player"english="Ok! Where do we start?"translation="هذا مقدور عليه... طيب! من أين نبدأ؟"/>
<dialoguespeaker="purple"english="Well, I've been trying to find them with the ship's scanners!"translation="حاولت أن أجدهم براصدات سفينتنا!"/>
<dialoguespeaker="purple"english="It's not working, but I did find something..."translation="لم ينفع ذلك، لكن وجدت أمرا يستحق الذكر..."/>
<dialoguespeaker="purple"english="These points show up on our scans as having high energy patterns!"translation="تلك النقاط في أرصادنا تشير إلى أنساق طاقة فائقة!"/>
<dialoguespeaker="purple"english="There's a good chance they're teleporters - which means they're probably built near something important..."translation="من المرجح أنها آلات تنقيل - يدل هذا أن مكان بنائها قرب شيء مهم..."/>
<dialoguespeaker="purple"english="They could be a very good place to start looking."translation="قد تكون تلك النقاط أفضل بداية لبحثنا."/>
<dialoguespeaker="player"english="Ok! I'll head out and see what I can find!"translation="واضح! إذن سأخرج وأرى ماذا عساي أجد!"/>
<dialoguespeaker="purple"english="I'll be right here if you need any help!"translation="سأنتظرك هنا في حال ما احتجت أي مساعدة!"/>
</cutscene>
<cutsceneid="bigopenworldskip"explanation="">
<dialoguespeaker="purple"english="I'll be right here if you need any help!"translation="سأنتظرك هنا في حال ما احتجت أي مساعدة!"/>
</cutscene>
<cutsceneid="talkpurple_intro"explanation="">
<dialoguespeaker="player"english="I'm feeling a bit overwhelmed, Doctor."translation="المهمة ضخمة، يا دكتورة."/>
<dialoguespeaker="player"english="Where do I begin?"translation="من أين أبدأ؟"/>
<dialoguespeaker="purple"english="Remember that you can press {b_map} to check where you are on the map!"translation="هل نسيت؟ يمكنك بضغط {b_map} التأكد من مكانك في الخريطة!"buttons="1"/>
<dialoguespeaker="purple"english="Look for areas where the rest of the crew might be..."translation="أقترح عليك البحث في الأماكن التي ربما نقل إليها بقية أفراد طاقمنا..."/>
<dialoguespeaker="purple"english="If you get lost, you can get back to the ship from any teleporter."translation="ولو ضعت، يمكنك العودة للسفينة من أي آلة تنقيل."/>
<dialoguespeaker="purple"english="And don't worry! We'll find everyone!"translation="ولا تشغلن بالك! مهما حصل سنعثر على الجميع!"/>
<dialoguespeaker="purple"english="Everything will be ok!"translation="وكل شيء سيمر على ما يرام...!"/>
</cutscene>
<cutsceneid="talkpurple_3"explanation="only one player string is shown">
<dialoguespeaker="purple"english="Are you doing ok, Captain?"translation="هل أنت بخير يا قبطان؟"/>
<dialoguespeaker="player"english="I'm worried about Victoria, Doctor!"translation="يقلقني أمر فيكتوريا يا دكتورة!"/>
<dialoguespeaker="player"english="I'm worried about Vitellary, Doctor!"translation="يقلقني أمر فيتيلاري يا دكتورة!"/>
<dialoguespeaker="player"english="I'm worried about Verdigris, Doctor!"translation="يقلقني أمر فيرديغري يا دكتورة!"/>
<dialoguespeaker="player"english="I'm worried about Vermilion, Doctor!"translation="يقلقني أمر فارميليون يا دكتورة!"/>
<dialoguespeaker="player"english="I'm worried about you, Doctor!"translation="يقلقني أمرك يا دكتورة!"/>
<dialoguespeaker="purple"english="Oh - well, don't worry, they'll show up!"translation="أوه، لا داع للقلق. عن قريب ستلتقيان بلا شك!"/>
<dialoguespeaker="purple"english="Here! Have a lollipop!"translation="هاك! مصاصة هدية مني!"/>
</cutscene>
<cutsceneid="trinketcollector"explanation="if no trinkets found yet">
<dialoguespeaker="cyan"english="This seems like a good place to store anything I find out there..."translation="يبدو مكانا مناسبا لتخزين أي شيء ألقاه..."/>
<dialoguespeaker="cyan"english="Victoria loves to study the interesting things we find on our adventures!"translation="كانت فيكتوريا مهتمة بدراسة ما نجلبه لها، كلما وجدنا ما يثير الاهتمام أثناء مغامراتنا!"/>
</cutscene>
<cutsceneid="newtrinketcollector"explanation="if at least one trinket found">
<dialoguespeaker="cyan"english="This seems like a good place to store those shiny things."translation="يبدو المكان مستودعا مناسبا لتلك الأغراض اللماعة."/>
<dialoguespeaker="cyan"english="Victoria loves to study the interesting things we find on our adventures!"translation="كانت فيكتوريا مهتمة بدراسة ما نجلبه لها، كلما وجدنا ما يثير الاهتمام أثناء مغامراتنا!"/>
</cutscene>
<cutsceneid="new2trinketcollector"explanation="">
<dialoguespeaker="cyan"english="I hope she's ok..."translation="أرجو أنها بخير..."/>
</cutscene>
<cutsceneid="rescuegreen"explanation="nuance: `she's BACK on the ship` means more `she's home` and not `she has returned to the ship`. She has never left the ship">
<dialoguespeaker="green"english="Captain! I've been so worried!"translation="قبطان! كم كنت قلقا!"/>
<dialoguespeaker="green"english="I've been trying to get out, but I keep going around in circles..."translation="حاولت الخروج، لكني كنت أدور وأدور وأعود من حيث بدأت..."/>
<dialoguespeaker="player"english="I've come from the ship. I'm here to teleport you back to it."translation="جئتك من السفينة. أتيت كي أرجعك إليها بآلة التنقيل."/>
<dialoguespeaker="green"english="Is everyone else alright? Is Violet..."translation="هل الجمع بخير؟ ماذا عن فايوليت، أهي..."/>
<dialoguespeaker="player"english="She's fine - she's back on the ship!"translation="لا تقلق عليها، عادت سالمة معافاة للسفينة!"/>
<dialoguespeaker="green"english="Oh! Great - Let's get going, then!"translation="أوه! عظيم إذن - فلننطلق دونما تأجيل!"/>
</cutscene>
<cutsceneid="rescueblue"explanation="">
<dialoguespeaker="blue"english="Oh no! Captain! Are you stuck here too?"translation="قبطان! مصيبة! حتى أنت علقت هنا؟"/>
<dialoguespeaker="player"english="It's ok - I'm here to rescue you!"translation="لا بأس - جئت لإنقاذك!"/>
<dialoguespeaker="player"english="Let me explain everything..."translation="فلتسمحي لي أن أشرح لك كل شيء..."/>
<dialoguespeaker="blue"english="What? I didn't understand any of that!"translation="كيف...؟ لم أفهم حرفا من كلامك!"/>
<dialoguespeaker="player"english="Oh... well, don't worry."translation="أوه... معذرة، المهم لا تقلقي."/>
<dialoguespeaker="player"english="Follow me! Everything will be alright!"translation="اتبعيني! ولا خوف عليك!"/>
<dialoguespeaker="red"english="Am I ever glad to see you! I thought I was the only one to escape the ship..."translation="ما أسعدني برؤيتك حقا! ظننت أني الناجي الوحيد من السفينة..."/>
<dialoguespeaker="player"english="Vermilion! I knew you'd be ok!"translation="فارميليون! لم يراودني شك أنك ستنجو!"/>
<dialoguespeaker="red"english="So, what's the situation?"translation="لكن، كيف الأوضاع؟"/>
<dialoguespeaker="red"english="I see! Well, we'd better get back then."translation="فهمت... إذن فلنتحرك ونرجع."/>
<dialoguespeaker="red"english="There's a teleporter in the next room."translation="توجد آلة تنقيل في الغرفة المجاورة."/>
</cutscene>
<cutsceneid="rescueyellow"explanation="">
<dialoguespeaker="yellow"english="Ah, Viridian! You got off the ship alright too?"translation="أه ، فيريديان! حتى أنت نجوت بدون إصابات من حادث السفينة؟"/>
<dialoguespeaker="player"english="It's good to see you're alright, Professor!"translation="أستاذ؟ يسعدني أنك بخير!"/>
<dialoguespeaker="yellow"english="Is the ship ok?"translation="هل السفينة بخير؟"/>
<dialoguespeaker="player"english="It's badly damaged, but Violet's been working on fixing it."translation="تعرضت لأضرار بليغة، لكن فايولت تعمل على إصلاحها."/>
<dialoguespeaker="player"english="We could really use your help..."translation="ستفيدنا مساعدتك جدا..."/>
<dialoguespeaker="yellow"english="Ah, of course!"translation="أه، عن طيب خاطر!"/>
<dialoguespeaker="yellow"english="The background interference in this dimension prevented the ship from finding a teleporter when we crashed!"translation="منع تشويش المحيط في نطاق هذا البعد سفينتنا من إيجاد آلة تنقيل قبيل الارتطام!"/>
<dialoguespeaker="yellow"english="We've all been teleported to different locations!"translation="تفرقنا ونقلنا إلى أماكن مختلفة!"/>
<dialoguespeaker="player"english="Er, that sounds about right!"translation="همم، يبدو لي كلامك منطقيا!"/>
<dialoguespeaker="yellow"english="Let's get back to the ship, then!"translation="فلنرجع إلى السفينة!"/>
<dialoguespeaker="yellow"english="After you, Captain!"translation="من بعدك يا قبطان!"/>
<dialoguespeaker="player"english="Something's gone wrong... We should look for a way back!"translation="حصلت مشكلة... فلنبحث عن طريق العودة!"/>
</cutscene>
<cutsceneid="int1blue_2"explanation="">
<dialoguespeaker="player"english="Follow me! I'll help you!"translation="اتبعيني! سأنقذك!"/>
<dialoguespeaker="blue"english="Promise you won't leave without me!"translation="أرجوك! أريد منك وعدا بعدم المغادرة وتركي!"/>
<dialoguespeaker="player"english="I promise! Don't worry!"translation="وعد! لا تقلقي!"/>
</cutscene>
<cutsceneid="int1blue_3"explanation="">
<dialoguespeaker="player"english="Are you ok down there, Doctor?"translation="هل نزلت على خير يا دكتورة؟"/>
<dialoguespeaker="blue"english="I wanna go home!"translation="آه، أريد العودة للديار!"/>
<dialoguespeaker="blue"english="Where are we? How did we even get here?"translation="أين نحن؟ كيف جئنا هنا؟"/>
<dialoguespeaker="player"english="Well, Violet did say that the interference in the dimension we crashed in was causing problems with the teleporters..."translation="صحيح، قالت فايولت أن في هذا البعد تشويشا يعرقل عمل آلات التنقيل..."/>
<dialoguespeaker="player"english="I guess something went wrong..."translation="فأظن مشكلة حصلت في الأثناء..."/>
<dialoguespeaker="player"english="But if we can find another teleporter, I think we can get back to the ship!"translation="لكن لو عثرنا على آلة تنقيل بديلة، يمكننا العودة للسفينة على ما أظن!"/>
<dialoguespeaker="blue"english="Captain! Captain! Wait for me!"translation="قبطان! قبطان! أرجوك الانتظار!"/>
<dialoguespeaker="blue"english="Please don't leave me behind! I don't mean to be a burden!"translation="أرجوك، لا أريد أن تتخلى عني! لا أقصد أن أكون عبئا عليك!"/>
<dialoguespeaker="blue"english="I'm scared!"translation="كل ما في الأمر... أني خائفة!"/>
<dialoguespeaker="player"english="Oh... don't worry Victoria, I'll look after you!"translation="أوه... لا تقلقي يا فيكتوريا، سأعتني بك!"/>
</cutscene>
<cutsceneid="int1blue_5"explanation="">
<dialoguespeaker="blue"english="We're never going to get out of here, are we?"translation="لن نخرج من هنا أبدا، أليس كذلك؟"/>
<dialoguespeaker="player"english="I.. I don't know..."translation="أنا... لا أدري..."/>
<dialoguespeaker="player"english="I don't know where we are or how we're going to get out..."translation="لا أعرف أين نحن، ولا كيف سنخرج..."/>
</cutscene>
<cutsceneid="int1blue_6"explanation="">
<dialoguespeaker="blue"english="We're going to be lost forever!"translation="سنضيع فيها إلى الأبد!"/>
<dialoguespeaker="player"english="Ok, come on... Things aren't that bad."translation="لحظة، صبرا... لا داع للتشاؤم إلى هذا الحد."/>
<dialoguespeaker="player"english="I have a feeling that we're nearly home!"translation="عندي إحساس أننا اقتربنا من طريق العودة للديار!"/>
<dialoguespeaker="player"english="We can't be too far from another teleporter!"translation="لسنا بعيدين عن آلة تنقيل أخرى بلا شك!"/>
<dialoguespeaker="blue"english="I hope you're right, captain..."translation="أتمنى أنك على حق يا قبطان..."/>
</cutscene>
<cutsceneid="int1blue_7"explanation="">
<dialoguespeaker="blue"english="Captain! You were right! It's a teleporter!"translation="قبطان! كلامك كان صحيحا! توجد آلة تنقيل!"/>
<dialoguespeaker="player"english="Phew! You had me worried for a while there... I thought we were never going to find one."translation="هوف! تنفست الصعداء بعد طول قلق... خشيت أننا لن نجد آلة تنقيل أخرى."/>
<dialoguespeaker="player"english="Anyway, let's go back to the ship."translation="على كل! فلنعد إلى السفينة."/>
</cutscene>
<cutsceneid="int1green_1"explanation="">
<dialoguespeaker="green"english="Huh? This isn't the ship..."translation="هاه؟ ليست هذه السفينة..."/>
<dialoguespeaker="green"english="Captain! What's going on?"translation="قبطان! ما الخطب؟"/>
<dialoguespeaker="player"english="I... I don't know!"translation="أنا... لا أعرف السبب!"/>
<dialoguespeaker="player"english="Where are we?"translation="أين نحن الآن؟"/>
<dialoguespeaker="green"english="Uh oh, this isn't good... Something must have gone wrong with the teleporter!"translation="أوه لا، لا يسر هذا... حصلت مشكلة في آلة التنقيل بلا شك!"/>
<dialoguespeaker="player"english="Ok... no need to panic!"translation="طيب... لا داعي للجزع!"/>
<dialoguespeaker="player"english="Let's look for another teleporter!"translation="فلنبحث عن آلة تنقيل أخرى!"/>
</cutscene>
<cutsceneid="int1green_2"explanation="">
<dialoguespeaker="player"english="Let's go this way!"translation="من هذا الطريق!"/>
<dialoguespeaker="green"english="After you, Captain!"translation="معك يا قبطان!"/>
</cutscene>
<cutsceneid="int1green_3"explanation="just like in rescuegreen, `Violet is back on the ship` does not mean she was ever off the ship and has *returned* to it. Or maybe Verdigris thinks Violet was warped off the ship as well just like him and she *has* made her way back to the ship?">
<dialoguespeaker="green"english="So Violet's back on the ship? She's really ok?"translation="إذن فايوليت عادت للسفينة؟ حقا هي بخير؟"/>
<dialoguespeaker="player"english="She's fine! She helped me find my way back!"translation="اطمئن! هي من ساعدتني على الاهتداء لطريق العودة!"/>
<dialoguespeaker="green"english="Oh, phew! I was worried about her."translation="أوه، هوف! خفت عليها."/>
<dialoguespeaker="green"english="Captain, I have a secret..."translation="قبطان، سأبوح لك بسر..."/>
<dialoguespeaker="green"english="I really like Violet!"translation="حقا أحب فايولت!"/>
<dialoguespeaker="player"english="Is that so?"translation="أحقا هذا؟"/>
<dialoguespeaker="green"english="Please promise you won't tell her!"translation="أترجاك عدم البوح بهذا السر أمامها!"/>
<dialoguespeaker="player"english="Are you doing ok?"translation="أأنت بخير؟"/>
<dialoguespeaker="green"english="I think so! I really hope we can find a way back to the ship..."translation="أظن! أرجو حقا أن نجد طريق العودة للسفينة..."/>
</cutscene>
<cutsceneid="int1green_5"explanation="">
<dialoguespeaker="green"english="So, about Violet..."translation="أردت سؤالك يا قبطان، بخصوص فايولت..."/>
<dialoguespeaker="player"english="So, do you think you'll be able to fix the ship?"translation="بالمناسبة، هل تقدر على إصلاح السفينة؟"/>
<dialoguespeaker="green"english="Depends on how bad it is... I think so, though!"translation="حسب فداحة الأضرار... لكني أظن أني على قدر المهمة!"/>
<dialoguespeaker="green"english="It's not very hard, really. The basic dimensional warping engine design is pretty simple, and if we can get that working we shouldn't have any trouble getting home."translation="ليس الأمر من الصعوبة بمكان. أما عن أساس فكرة التصميم لمحرك التنقيل ما بين الأبعاد، فمسألته بسيطة، وإن شغلناه فلن نلقى عقبة في العودة للديار."/>
<dialoguespeaker="green"english="Finally! A teleporter!"translation="أخيرا! آلة تنقيل!"/>
<dialoguespeaker="green"english="I was getting worried we wouldn't find one..."translation="بدأت أقلق وأتساءل ماذا لو لم نجدها..."/>
<dialoguespeaker="player"english="Let's head back to the ship!"translation="فلنرجع إلى السفينة!"/>
</cutscene>
<cutsceneid="int1red_1"explanation="">
<dialoguespeaker="red"english="Wow! Where are we?"translation="واو! أين نحن؟"/>
<dialoguespeaker="player"english="This... isn't right... Something must have gone wrong with the teleporter!"translation="لا... لا يصح هذا... حصلت مشكلة في آلة التنقيل بدون شك!"/>
<dialoguespeaker="red"english="Oh well... We can work it out when we get back to the ship!"translation="حقا... لكن لا بأس، سنتوصل إلى جواب بعد أن نرجع إلى السفينة!"/>
<dialoguespeaker="red"english="Let's go exploring!"translation="فلننطلق في رحلة استكشاف جديدة!"/>
<dialoguespeaker="player"english="Ok then!"translation="هيا بنا إذن!"/>
<dialoguespeaker="red"english="Aye aye, Captain!"translation="لا يأمر عليك ظالم، يا قبطان!"/>
</cutscene>
<cutsceneid="int1red_3"explanation="">
<dialoguespeaker="red"english="Hey Viridian... how did the crash happen, exactly?"translation="ها فيريديان... كيف حصل حادث الارتطام بالضبط؟"/>
<dialoguespeaker="player"english="Oh, I don't really know - some sort of interference..."translation="أوه، لا أدري عن التفاصيل - كأنها إشارة تشويش..."/>
<dialoguespeaker="player"english="...or something sciencey like that. It's not really my area."translation="...أو كلام علماء من هذا النوع. خارج عن تخصصي."/>
<dialoguespeaker="red"english="Ah! Well, do you think we'll be able to fix the ship and go home?"translation="أه! طيب، هل تظننا سنتمكن من إصلاح السفينة والعودة للديار؟"/>
<dialoguespeaker="player"english="Of course! Everything will be ok!"translation="طبعا بلا شك! ستحل كل الأمور!"/>
<dialoguespeaker="red"english="Hi again! You doing ok?"translation="أهلا مجددا! أحوالك بخير؟"/>
<dialoguespeaker="player"english="I think so! But I really want to get back to the ship..."translation="أظن! لكني بدأت أشتاق جدا للسفينة..."/>
<dialoguespeaker="red"english="We'll be ok! If we can find a teleporter somewhere we should be able to get back!"translation="لا خوف علينا! لو عثرنا على آلة تنقيل سنتمكن من العودة!"/>
</cutscene>
<cutsceneid="int1red_5"explanation="">
<dialoguespeaker="red"english="Are we there yet?"translation="هل وصلنا أم ما زلنا؟"/>
<dialoguespeaker="player"english="We're getting closer, I think..."translation="أظننا نقترب..."/>
<dialoguespeaker="player"english="I wonder where we are, anyway?"translation="يا ترى أين نحن أصلا؟"/>
<dialoguespeaker="player"english="This seems different from that dimension we crashed in, somehow..."translation="يبدو هذا النطاق مختلفا عن البعد الذي سقطنا فيه، لا أدري كيف..."/>
<dialoguespeaker="red"english="I dunno... But we must be close to a teleporter by now..."translation="ما أدراني... لكن لا شك أننا اقتربنا من آلة تنقيل بعد هذا المسير..."/>
<dialoguespeaker="red"english="See? I told you! Let's get back to the ship!"translation="رأيت؟ قلت لك! فلنرجع للسفينة!"/>
</cutscene>
<cutsceneid="int1yellow_1"explanation="">
<dialoguespeaker="yellow"english="Oooh! This is interesting..."translation="أوووه! مثير..."/>
<dialoguespeaker="yellow"english="Captain! Have you been here before?"translation="قبطان! هل سبق وجئت إلى هنا؟"/>
<dialoguespeaker="player"english="What? Where are we?"translation="ما هذا... أين نحن؟"/>
<dialoguespeaker="yellow"english="I suspect something deflected our teleporter transmission! This is somewhere new..."translation="يخامرني شك أن شيئا ما أثر في بث تنقيلتنا وأحادها عن مسارها! هذا المكان جديد علينا..."/>
<dialoguespeaker="player"english="We should try to find a teleporter and get back to the ship..."translation="فلنحاول أن نجد آلة تنقيل حتى نرجع للسفينة..."/>
<dialoguespeaker="yellow"english="Right behind you, Captain!"translation="على خطاك نسير، يا قبطان!"/>
</cutscene>
<cutsceneid="int1yellow_3"explanation="">
<dialoguespeaker="player"english="What do you make of all this, Professor?"translation="...بروفيسور، ما استنتاجاتك؟"/>
<dialoguespeaker="yellow"english="I'm guessing this dimension has something to do with the interference that caused us to crash!"translation="أخمن أن لهذا البعد يدا في سبب التشويش الذي سبب حادث ارتطام سفينتنا!"/>
<dialoguespeaker="yellow"english="Maybe we'll find the cause of it here?"translation="لعلنا نلقى أصل السبب هنا؟"/>
<dialoguespeaker="yellow"english="Well, it's just a guess. I'll need to get back to the ship before I can do any real tests..."translation="لكنه تخمين لا أكثر. أحتاج العودة للسفينة قبل أن أجري أي اختبارات جادة..."/>
</cutscene>
<cutsceneid="int1yellow_4"explanation="the split paths merge, and Vitellary sees a checkpoint">
<dialoguespeaker="yellow"english="Ohh! What was that?"translation="أووو! ما هذا؟"/>
<dialoguespeaker="player"english="What was what?"translation="ما هذا الذي تقول عنده ما هذا؟"/>
<dialoguespeaker="yellow"english="That big... C thing! I wonder what it does?"translation="تلك... الحاء الكبيرة! في ذلك الشيء! ماذا يفعل يا ترى؟"/>
<dialoguespeaker="player"english="Em... I don't really know how to answer that question..."translation="احم... لا أعرف كيف أجيبك على سؤالك هذا..."/>
<dialoguespeaker="player"english="It's probably best not to acknowledge that it's there at all."translation="الأفضل أن نتجاهل وجوده، ونمر عليه مرور الكرام."/>
<dialoguespeaker="yellow"english="Maybe we should take it back to the ship to study it?"translation="ألا يجدر بنا جلبه للسفينة حتى نجري عليه دراسات؟"/>
<dialoguespeaker="player"english="We really shouldn't think about it too much... Let's keep moving!"translation="حقا من الأفضل ألا نفرط في التفكير في أمره... ولنتحرك!"/>
</cutscene>
<cutsceneid="int1yellow_5"explanation="">
<dialoguespeaker="yellow"english="You know, there's something really odd about this dimension..."translation="لو علمت بم أفكر الآن... أحس أن في هذا البعد أمرا في غاية الغرابة..."/>
<dialoguespeaker="yellow"english="We shouldn't really be able to move between dimensions with a regular teleporter..."translation="لا يفترض بنا أن نتمكن من التنقل بين الأبعاد بمجرد آلة تنقيل عادية..."/>
<dialoguespeaker="yellow"english="Maybe this isn't a proper dimension at all?"translation="ألا يحتمل إذن أن هذا ليس ببعد قائم الذات؟"/>
<dialoguespeaker="yellow"english="Maybe it's some kind of polar dimension? Something artificially created for some reason?"translation="ربما هو بعد قطبي، أو شيء من ذلك النوع؟ بمعنى أنه شيء كونوه اصطناعيا لسبب من الأسباب؟"/>
<dialoguespeaker="yellow"english="I can't wait to get back to the ship. I have a lot of tests to run!"translation="لا أطيق الصبر على العودة للسفينة. كم عندي اختبارات كثيرة أجريها!"/>
</cutscene>
<cutsceneid="int1yellow_6"explanation="">
<dialoguespeaker="yellow"english="I wonder if there's anything else in this dimension worth exploring?"translation="أتساءل هل في هذا البعد مكانا آخرا يستحق الاستطلاع؟"/>
<dialoguespeaker="player"english="Maybe... but we should probably just focus on finding the rest of the crew for now..."translation="ربما... لكن يجدر بنا أن نركز على إنجاد بقية أفراد طاقمنا، ثم للحديث بقية..."/>
<dialoguespeaker="yellow"english="That was interesting, wasn't it?"translation="ألم تكن نزهة مثيرة للحماس؟"/>
<dialoguespeaker="player"english="I feel dizzy..."translation="آه، يا للدوار..."/>
</cutscene>
<cutsceneid="talkpurple_1"explanation="">
<dialoguespeaker="purple"english="... I hope Verdigris is alright."translation="... أرجو أن فارديغري بخير."/>
<dialoguespeaker="purple"english="If you can find him, he'd be a big help fixing the ship!"translation="ليتك وجدته، لأنه سيكون عونا كبيرا في إصلاح السفينة!"/>
</cutscene>
<cutsceneid="talkpurple_2"explanation="">
<dialoguespeaker="purple"english="Chief Verdigris is so brave and ever so smart!"translation="كم القائد فيرديغري شجاع، وشهم، وذكي على الدوام!"/>
</cutscene>
<cutsceneid="talkpurple_4"explanation="">
<dialoguespeaker="purple"english="Welcome back, Captain!"translation="أهلا بعودتك بالسلامة يا قبطان!"/>
<dialoguespeaker="purple"english="I think Victoria is quite happy to be back on the ship."translation="أحسب أن فيكتوريا سعيدة جدا بعودتها للسفينة."/>
<dialoguespeaker="purple"english="She really doesn't like adventuring. She gets very homesick!"translation="لا تحب أجواء المغامرات كثيرا. لأنها سرعان ما تحن للبيت!"/>
</cutscene>
<cutsceneid="talkpurple_5"explanation="only one of the last 6 strings is shown">
<dialoguespeaker="purple"english="Vermilion called in to say hello!"translation="اتصل فارميليون، ويسلم عليك!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find the rest of the crew!"translation="يتوق حقا لمساعدتك في إيجاد بقية الطاقم!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Victoria!"translation="يتوق حقا لمساعدتك في إيجاد فيكتوريا!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Vitellary!"translation="يتوق حقا لمساعدتك في إيجاد فيتيلاري!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Verdigris!"translation="يتوق حقا لمساعدتك في إيجاد فارديغري!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Vermilion!"translation="يتوق حقا لمساعدتك في إيجاد فارميليون!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find you!"translation="يتوق حقا لمساعدتك في التوصل لإيجاد مكانك!"/>
</cutscene>
<cutsceneid="talkpurple_6"explanation="">
<dialoguespeaker="purple"english="Captain! You found Verdigris!"translation="قبطان! عثرت على فارديغري!"/>
<dialoguespeaker="purple"english="Thank you so much!"translation="حقا أشكرك! شكرا جزيلا!"/>
</cutscene>
<cutsceneid="talkpurple_7"explanation="">
<dialoguespeaker="purple"english="I'm glad Professor Vitellary is ok!"translation="سعيدة أن البروفيسور فيتيلاري بخير!"/>
<dialoguespeaker="purple"english="He had lots of questions for me about this dimension."translation="سألني أسئلة كثيرة عن هذا البعد."/>
<dialoguespeaker="purple"english="He's already gotten to work with his research!"translation="وسرعان ما عاد إلى العمل على أبحاثه!"/>
<dialoguespeaker="player"english="Doctor, something strange happened when we teleported back to the ship..."translation="دكتور، حصل شيء غريب عندما انتقلنا راجعين للسفينة..."/>
<dialoguespeaker="player"english="We got lost in another dimension!"translation="تهنا في بعد آخر!"/>
<dialoguespeaker="purple"english="Maybe that dimension has something to do with the interference that caused us to crash here?"translation="ربما لذلك البعد دخل في التشويش الذي تسبب في حادث ارتطامنا؟"/>
<dialoguespeaker="purple"english="I'll look into it..."translation="سأنظر في الموضوع..."/>
<dialoguespeaker="player"english="Doctor, something strange has been happening when we teleport back to the ship..."translation="دكتور، حصل شيء غريب عندما انتقلنا راجعين للسفينة..."/>
<dialoguespeaker="player"english="We keep getting brought to another weird dimension!"translation="يوجد بعد غريب، وأرغمنا مجددا على الانتقال إليه!"/>
<dialoguespeaker="purple"english="Maybe that dimension has something to do with the interference that caused us to crash here?"translation="ربما لذلك البعد دخل في التشويش الذي تسبب في حادث ارتطامنا؟"/>
<dialoguespeaker="purple"english="Hmm, there's definitely something strange happening..."translation="همممم، بالتأكيد يحصل شيء غريب..."/>
<dialoguespeaker="purple"english="If only we could find the source of that interference!"translation="ليتنا نتوصل إلى مصدر ذلك التشويش!"/>
</cutscene>
<cutsceneid="talkpurple_8"explanation="">
<dialoguespeaker="purple"english="Hey Captain! Now that you've turned off the source of the interference, we can warp everyone back to the ship instantly, if we need to!"translation="هيه يا قبطان! بعد أن أغلقت مصدر التشويش، صرنا نستطيع أن ننقل الجميع إلى متن السفينة فور ما نشاء، وقتما نحتاج!"/>
<dialoguespeaker="purple"english="Any time you want to come back to the ship, just select the new SHIP option in your menu!"translation="متى أردت العودة للسفينة، يمكنك اختيار خيار السفينة في القائمة!"/>
</cutscene>
<cutsceneid="talkgreen_1"explanation="">
<dialoguespeaker="green"english="I'm an engineer!"translation="أنا مهندس!"/>
</cutscene>
<cutsceneid="talkgreen_2"explanation="">
<dialoguespeaker="green"english="I think I can get this ship moving again, but it's going to take a while..."translation="أستطيع جعل السفينة تتحرك ثانية، لكن عملي سيستغرق فترة..."/>
</cutscene>
<cutsceneid="talkgreen_3"explanation="">
<dialoguespeaker="green"english="Victoria mentioned something about a lab? I wonder if she found anything down there?"translation="حكت لك فايوليت عن مخبر؟ ترى هل وجدت فيه شيئا؟"/>
</cutscene>
<cutsceneid="talkgreen_4"explanation="">
<dialoguespeaker="green"english="Vermilion's back! Yey!"translation="فارميليون عاد! يا لفرحتي، ياي!!"/>
</cutscene>
<cutsceneid="talkgreen_5"explanation="">
<dialoguespeaker="green"english="The Professor had lots of questions about this dimension for me..."translation="سألني البروفيسور أسئلة كثيرة بخصوص هذا البعد..."/>
<dialoguespeaker="green"english="We still don't really know that much, though."translation="ومع هذا لا زلنا لا نعرف الكثير."/>
<dialoguespeaker="green"english="Until we work out what's causing that interference, we can't go anywhere."translation="إلى أن نتوصل لمعرفة ما الذي يتسبب بالتداخل، لن نستطيع التحرك قيد أنملة."/>
</cutscene>
<cutsceneid="talkgreen_6"explanation="">
<dialoguespeaker="green"english="I'm so glad that Violet's alright!"translation="كم أنا سعيد أن فايوليت بخير!"/>
</cutscene>
<cutsceneid="talkgreen_7"explanation="">
<dialoguespeaker="green"english="That other dimension we ended up in must be related to this one, somehow..."translation="هل تذكرت ذلك البعد البديل الذي انتهى المطاف بنا إليه؟ أظن بينه وهذا البعد علاقة ما..."/>
</cutscene>
<cutsceneid="talkgreen_8"explanation="">
<dialoguespeaker="green"english="The antenna's broken! This is going to be very hard to fix..."translation="الهوائي مكسور! سيصعب التصليح جدا..."/>
</cutscene>
<cutsceneid="talkgreen_9"explanation="">
<dialoguespeaker="green"english="It looks like we were warped into solid rock when we crashed!"translation="يبدو أننا عندما ارتطمنا هنا، حصلت تنقيلة للسفينة إلى داخل كتلة صخرية!"/>
<dialoguespeaker="green"english="Hmm. It's going to be hard to separate from this..."translation="هممم. سيصعب فصل السفينة عنها..."/>
</cutscene>
<cutsceneid="talkgreen_10"explanation="">
<dialoguespeaker="green"english="The ship's all fixed up. We can leave at a moment's notice!"translation="أصلحت السفينة بأكملها. يمكننا المغادرة في أي لحظة، والأمر رهن إشارتك!"/>
</cutscene>
<cutsceneid="talkred_1"explanation="">
<dialoguespeaker="red"english="Don't worry, Sir!"translation="لا قلق، يا فندم!"/>
<dialoguespeaker="red"english="We'll find a way out of here!"translation="سنبحث عن طريق للخروج من هنا!"/>
</cutscene>
<cutsceneid="talkred_2"explanation="">
<dialoguespeaker="red"english="I hope Victoria is ok..."translation="أرجو أن فيكتوريا بخير..."/>
<dialoguespeaker="red"english="She doesn't handle surprises very well..."translation="أعرفها، ولا تتحمل المفاجآت جيدا..."/>
</cutscene>
<cutsceneid="talkred_3"explanation="">
<dialoguespeaker="red"english="I don't know how we're going to get this ship working again!"translation="لا أدري كيف سنتصرف لتشغيل هذه السفينة مجددا!"/>
<dialoguespeaker="red"english="Chief Verdigris would know what to do..."translation="لو كان القائد فارديغري هنا، لكان يعرف الحل..."/>
</cutscene>
<cutsceneid="talkred_4"explanation="">
<dialoguespeaker="red"english="I wonder what caused the ship to crash here?"translation="يا ترى ما الذي سبب ارتطام السفينة في هذا المكان؟"/>
<dialoguespeaker="red"english="It's the shame the Professor isn't here, huh? I'm sure he could work it out!"translation="خسارة أن البروفيسور ليس هنا. أليس كذلك؟ أنا متأكد أنه لن يحير في إيجاد الجواب!"/>
</cutscene>
<cutsceneid="talkred_5"explanation="">
<dialoguespeaker="red"english="It's great to be back!"translation="ما أحلى العودة!"/>
<dialoguespeaker="red"english="I can't wait to help you find the rest of the crew!"translation="أتوق بفارغ الصبر لمساعدتك على إيجاد بقية الطاقم!"/>
<dialoguespeaker="red"english="It'll be like old times, huh, Captain?"translation="مثل الأيام الخوالي، أليس كذلك يا قبطان؟"/>
</cutscene>
<cutsceneid="talkred_6"explanation="">
<dialoguespeaker="red"english="It's good to have Victoria back with us."translation="جيد أن فيكتوريا عادت إلينا."/>
<dialoguespeaker="red"english="She really seems happy to get back to work in her lab!"translation="بدت لي في غاية السعادة عندما رجعت للعمل في مخبرها!"/>
</cutscene>
<cutsceneid="talkred_7"explanation="">
<dialoguespeaker="red"english="I think I saw Verdigris working on the outside of the ship!"translation="أظن أني رأيت فارديغري منهمكا في العمل خارج السفينة!"/>
</cutscene>
<cutsceneid="talkred_8"explanation="">
<dialoguespeaker="red"english="You found Professor Vitellary! All right!"translation="وجدت البروفيسور فيتيلاري! أحلى الأخبار!"/>
<dialoguespeaker="red"english="We'll have this interference thing worked out in no time now!"translation="ما دام الأمر هكذا، فسرعان ما سنحل مشكلة التشويش هذه!"/>
</cutscene>
<cutsceneid="talkred_9"explanation="">
<dialoguespeaker="red"english="That other dimension was really strange, wasn't it?"translation="كان ذلك البعد البديل في غاية الغرابة، أليس كذلك؟"/>
<dialoguespeaker="red"english="I wonder what caused the teleporter to send us there?"translation="ترى ما الذي جعل آلة التنقيل ترسلنا إلى هناك؟"/>
</cutscene>
<cutsceneid="talkred_10"explanation="">
<dialoguespeaker="red"english="Heya Captain!"translation="ويه يا قبطان!"/>
<dialoguespeaker="red"english="This way looks a little dangerous..."translation="هذا الطريق يبدو لي خطيرا قليلا..."/>
<dialoguespeaker="red"english="Hey Captain!"translation="أهلا يا قبطان!"/>
<dialoguespeaker="red"english="I found something interesting around here - the same warp signature I saw when I landed!"translation="عثرت على شيء مثير للاهتمام هنا - نفس بصمة الطاقة لآلة التنقيل التي رأيتها عندما نزلت!"/>
<dialoguespeaker="red"english="Someone from the ship must be nearby..."translation="لا بد أن أحد أفراد طاقم السفينة قربنا..."/>
</cutscene>
<cutsceneid="talkred_13"explanation="">
<dialoguespeaker="red"english="This dimension is pretty exciting, isn't it?"translation="هذا البعد حافل بالتشويق والإثارة، صح أو لا؟"/>
<dialoguespeaker="red"english="I wonder what we'll find?"translation="يا ترى ماذا سنلقى؟"/>
</cutscene>
<cutsceneid="talkblue_1"explanation="">
<dialoguespeaker="blue"english="Any signs of Professor Vitellary?"translation="هل من علامات تدل على مكان البروفيسور فيتيلاري؟"/>
<dialoguespeaker="player"english="Sorry, not yet..."translation="معذرة، لم أجده بعد..."/>
<dialoguespeaker="blue"english="I hope he's ok..."translation="أتمنى أنه بخير..."/>
</cutscene>
<cutsceneid="talkblue_2"explanation="">
<dialoguespeaker="blue"english="Thanks so much for saving me, Captain!"translation="شكرا من القلب لإنقاذي يا قبطان!"/>
</cutscene>
<cutsceneid="talkblue_3"explanation="">
<dialoguespeaker="blue"english="I'm so glad to be back!"translation="كم أنا سعيدة بعودتي!"/>
<dialoguespeaker="blue"english="That lab was so dark and scary! I didn't like it at all..."translation="كم كان ذلك المخبر مظلما... موحشا... مخيفا! لم أستلطفه إطلاقا..."/>
</cutscene>
<cutsceneid="talkblue_4"explanation="">
<dialoguespeaker="blue"english="Vitellary's back? I knew you'd find him!"translation="عاد فيتيلاري؟ كنت واثقة أنكما ستلتقيان!"/>
<dialoguespeaker="blue"english="I mean, I admit I was very worried that you wouldn't..."translation="بل أعترف... أني خفت جدا أنك ربما ذهبت ورجعت دون إيجاده..."/>
<dialoguespeaker="blue"english="or that something might have happened to him..."translation="أو أن مكروها حصل له، حاشا وكلا..."/>
<dialoguespeaker="player"english="Doctor Victoria? He's ok!"translation="دكتورة فيكتوريا؟ هوني عليك. لم يحصل شر له، فلا داعي للقلق!"/>
<dialoguespeaker="blue"english="Oh! Sorry! I was just thinking about what if he wasn't?"translation="أوه! متأسفة! تخيلت أنه لم يكن بخير، ماذا كنت لأفعل وقتها؟"/>
<dialoguespeaker="blue"english="Thank you, Captain!"translation="أشكرك جزيلا، يا قبطان!"/>
</cutscene>
<cutsceneid="talkblue_5"explanation="">
<dialoguespeaker="blue"english="You found Vermilion! Great!"translation="وجدت فارميليون! هذا خبر رائع!"/>
<dialoguespeaker="blue"english="I wish he wasn't so reckless!"translation="ليته يقلل من طيشه!"/>
<dialoguespeaker="blue"english="He'll get himself into trouble..."translation="سيودي به تهوره إلى مشكلة كبيرة..."/>
</cutscene>
<cutsceneid="talkblue_6"explanation="">
<dialoguespeaker="blue"english="Verdigris is ok! Violet will be so happy!"translation="فارديغري بخير! ستسعد فايولت كثيرا!"/>
<dialoguespeaker="blue"english="Though I was very worried..."translation="مع ذلك كنت قلقة جدا..."/>
</cutscene>
<cutsceneid="talkblue_7"explanation="">
<dialoguespeaker="blue"english="Why did the teleporter send us to that scary dimension?"translation="لماذا أرسلتنا آلة التنقيل تلك إلى ذلك البعد الموحش؟"/>
<dialoguespeaker="blue"english="What happened?"translation="ماذا حصل وقتها؟"/>
<dialoguespeaker="player"english="I don't know, Doctor..."translation="لا أدري يا دكتورة..."/>
<dialoguespeaker="blue"english="Heya Captain!"translation="يا أهلا يا قبطان!"/>
<dialoguespeaker="blue"english="Are you going to try and find the rest of these shiny things?"translation="هل نويت محاولة جمع بقية هذه الأغراض اللماعة؟"/>
</cutscene>
<cutsceneid="talkblue_trinket1"explanation="">
<dialoguespeaker="blue"english="Hey Captain, I found this in that lab..."translation="أهلا يا قبطان، وجدت هذا في ذلك المخبر..."/>
<dialoguespeaker="blue"english="Any idea what it does?"translation="هل عندك فكرة بم يفيدنا؟"/>
<dialoguespeaker="player"english="Sorry, I don't know!"translation="أتأسف، لا أدري!"/>
<dialoguespeaker="player"english="They seem important, though..."translation="لكنه يبدو مهما..."/>
<dialoguespeaker="player"english="Maybe something will happen if we find them all?"translation="ربما لو جمعناهم كلهم يحصل شيء؟"/>
</cutscene>
<cutsceneid="talkblue_trinket2"explanation="">
<dialoguespeaker="blue"english="Captain! Come have a look at what I've been working on!"translation="قبطان! عندي شيء كنت أعمل عليه، لو أردت رؤيته هيا!"/>
<dialoguespeaker="blue"english="It looks like these shiny things are giving off a strange energy reading!"translation="يبدو أن تلك اللماعات الغريبات ترصد منها إشارة طاقة غريبة!"/>
<dialoguespeaker="blue"english="So I analysed it..."translation="ولهذا السبب أجريت دراسة تحليلية عليها..."/>
</cutscene>
<cutsceneid="talkblue_trinket3"explanation="">
<dialoguespeaker="blue"english="Captain! Come have a look at what I've been working on!"translation="قبطان! عندي شيء كنت أعمل عليه، لو أردت رؤيته هيا!"/>
<dialoguespeaker="blue"english="I found this in that lab..."translation="وجدت هذا في ذلك المخبر..."/>
<dialoguespeaker="blue"english="It seemed to be giving off a weird energy reading..."translation="مما بدا لي، صدر منه إشارة طاقة غريبة..."/>
<dialoguespeaker="blue"english="So I analysed it..."translation="ولهذا السبب أجريت دراسة تحليلية عليه..."/>
</cutscene>
<cutsceneid="talkblue_trinket4"explanation="">
<dialoguespeaker="blue"english="...and I was able to find more of them with the ship's scanner!"translation="...واستطعت أن أجد المزيد منهم بفضل ماسحة السفينة!"/>
<dialoguespeaker="blue"english="If you get a chance, it might be worth finding the rest of them!"translation="لو سنحت لك الفرصة، ربما يستحق العناء جمع بقية تلك الأغراض!"/>
<dialoguespeaker="blue"english="Don't put yourself in any danger, though!"translation="لكن... إياك وتعريض نفسك للأخطار!"/>
</cutscene>
<cutsceneid="talkblue_trinket5"explanation="">
<dialoguespeaker="blue"english="...but it looks like you've already found all of them in this dimension!"translation="...إلا أنك وجدتها كلها في هذا البعد قبل حديثنا!"/>
<dialoguespeaker="blue"english="Yeah, well done! That can't have been easy!"translation="أجل، أحسنت حقا! لا أظن ذلك كان بالمهمة السهلة!"/>
</cutscene>
<cutsceneid="talkblue_trinket6"explanation="">
<dialoguespeaker="blue"english="...and they're related. They're all a part of something bigger!"translation="...كما أن علاقة تجمع بينها. كل منها [بعض] من [كل]!"/>
<dialoguespeaker="blue"english="Yeah! There seem to be twenty variations of the fundamental energy signature..."translation="أجل! يبدو أن للإشارة الأساسية الطاقية عشرون نوعا بتغييرات طفيفة..."/>
<dialoguespeaker="blue"english="Does that mean you've found all of them?"translation="هل عنيت بكلامك أنك وجدتها كلها؟"/>
</cutscene>
<cutsceneid="talkyellow_1"explanation="">
<dialoguespeaker="yellow"english="I'm making some fascinating discoveries, captain!"translation="توصلت إلى اكتشافات مدهشة، يا قبطان!"/>
</cutscene>
<cutsceneid="talkyellow_2"explanation="">
<dialoguespeaker="yellow"english="This isn't like any other dimension we've been to, Captain."translation="هذا البعد لا يشبه الأبعاد الأخرى التي زرناها يا قبطان."/>
<dialoguespeaker="yellow"english="There's something strange about this place..."translation="في هذا المكان أمر غريب..."/>
</cutscene>
<cutsceneid="talkyellow_3"explanation="">
<dialoguespeaker="yellow"english="Captain, have you noticed that this dimension seems to wrap around?"translation="قبطان، هل لاحظت أن هذا البعد يبدو وكأنه يلتف حول نفسه؟"/>
<dialoguespeaker="yellow"english="It looks like this dimension is having the same stability problems as our own!"translation="يبدو أن هذا البعد يتشاطر مع بعدنا في ديارنا نفس مشاكل الاستقرار!"/>
<dialoguespeaker="yellow"english="I hope we're not the ones causing it..."translation="أرجو أننا لسنا المتسببين بذلك..."/>
<dialoguespeaker="player"english="What? Do you think we might be?"translation="ماذا؟ أتظننا سببا؟"/>
<dialoguespeaker="yellow"english="No no... that's very unlikely, really..."translation="لا لا... ذلك احتمال ضئيل، لا يؤخذ به حقا..."/>
</cutscene>
<cutsceneid="talkyellow_4"explanation="">
<dialoguespeaker="yellow"english="My guess is that whoever used to live here was experimenting with ways to stop the dimension from collapsing."translation="حسب تخميناتي، فمن كانوا يعيشون هنا أجروا تجارب كثيرة وبحثوا في طرق إيقاف انهيار هذا البعد."/>
<dialoguespeaker="yellow"english="It would explain why they've wrapped the edges..."translation="يفسر هذا لماذا حاولوا ربط الحواف ببعضها..."/>
<dialoguespeaker="yellow"english="Hey, maybe that's what's causing the interference?"translation="هيه، ربما ذلك ما يسبب التشويش؟"/>
</cutscene>
<cutsceneid="talkyellow_5"explanation="">
<dialoguespeaker="yellow"english="I wonder where the people who used to live here have gone?"translation="أتساءل إلى أين سافر الناس الذين كانوا يعيشون هنا؟"/>
</cutscene>
<cutsceneid="talkyellow_6"explanation="">
<dialoguespeaker="yellow"english="I think it's no coincidence that the teleporter was drawn to that dimension..."translation="لا أظنها صدفة أن آلة التنقيل استقطبتنا إلى هذا البعد..."/>
<dialoguespeaker="yellow"english="There's something there. I think it might be causing the interference that's stopping us from leaving..."translation="يوجد هناك شيء. وأظنه السبب في التشويش الذي يمنعنا من المغادرة..."/>
</cutscene>
<cutsceneid="talkyellow_7"explanation="">
<dialoguespeaker="yellow"english="I'm glad Verdigris is alright."translation="سعيد أن فارديغري بخير."/>
<dialoguespeaker="yellow"english="It'll be a lot easier to find some way out of here now that we can get the ship working again!"translation="سيسهل كثيرا إيجاد مخرج من هنا بعد أن صارت السفينة تعمل!"/>
</cutscene>
<cutsceneid="talkyellow_8"explanation="">
<dialoguespeaker="yellow"english="Ah, you've found Doctor Victoria? Excellent!"translation="أه، وجدت الدكتورة فيكتوريا! أخبار رائعة!"/>
<dialoguespeaker="yellow"english="I have lots of questions for her!"translation="عندي أسئلة كثيرة أطرحها عليها!"/>
</cutscene>
<cutsceneid="talkyellow_9"explanation="">
<dialoguespeaker="yellow"english="Vermilion says that he was trapped in some sort of tunnel?"translation="قال فارميليون أنه كان عالقا في نفق ما؟"/>
<dialoguespeaker="player"english="Yeah, it just seemed to keep going and going..."translation="أجل، بدا وكأنه يستمر بلا نهاية..."/>
<dialoguespeaker="yellow"english="Interesting... I wonder why it was built?"translation="معلومة تفيد... يا ترى ما سبب بنائه على تلك الحالة؟"/>
</cutscene>
<cutsceneid="talkyellow_10"explanation="">
<dialoguespeaker="yellow"english="It's good to be back!"translation="ما أحلى العودة!"/>
<dialoguespeaker="yellow"english="I've got so much work to catch up on..."translation="عندي شغل كثير فاتني يجب أن أتداركه..."/>
</cutscene>
<cutsceneid="talkyellow_11"explanation="">
<dialoguespeaker="yellow"english="I know it's probably a little dangerous to stay here now that this dimension is collapsing..."translation="أعرف أن البقاء في هذا البعد، وهو آهل للانهيار، مجازفة بعض الشيء..."/>
<dialoguespeaker="yellow"english="...but it's so rare to find somewhere this interesting!"translation="...لكن يندر أن تجد في الحياة أمرا مثيرا للاهتمام بنفس القدر!"/>
<dialoguespeaker="yellow"english="Maybe we'll find the answers to our own problems here?"translation="ربما قد نجد هنا حلولا للمشاكل التي اعترضتنا في ديارنا؟"/>
</cutscene>
<cutsceneid="talkyellow_trinket1"explanation="">
<dialoguespeaker="yellow"english="Captain! I've been meaning to give this to you..."translation="قبطان! أردت أن أعطيك هذا..."/>
<dialoguespeaker="player"english="Professor! Where did you find this?"translation="بروفيسور! من أين لك هذا؟"/>
<dialoguespeaker="yellow"english="Oh, it was just lying around that space station."translation="أوه، كان غرضا منسيا في تلك المحطة الفضائية."/>
<dialoguespeaker="yellow"english="It's a pity Doctor Victoria isn't here, she loves studying that sort of thing..."translation="خسارة أن فيكتوريا ليست هنا، لأنها تعشق دراسة تلك الأشياء..."/>
<dialoguespeaker="player"english="Any idea what it does?"translation="هل عندك فكرة عن فائدته؟"/>
<dialoguespeaker="yellow"english="Nope! But it is giving off a strange energy reading..."translation="أبدا! لكنه يصدر إشارة طاقة غريبة.."/>
</cutscene>
<cutsceneid="talkyellow_trinket2"explanation="">
<dialoguespeaker="yellow"english="...so I used the ship's scanner to find more of them!"translation="...ولهذا السبب استغليت راصدات السفينة كي أبحث عن المزيد منه!"/>
<dialoguespeaker="yellow"english="...Please don't let them distract you from finding Victoria, though!"translation="...لكن أتمنى ألا يشتت هذا اهتمامك عن إنقاذ فيكتوريا!"/>
<dialoguespeaker="yellow"english="I hope she's ok..."translation="أرجو أنها بخير..."/>
</cutscene>
<cutsceneid="talkyellow_trinket3"explanation="">
<dialoguespeaker="yellow"english="Can't seem to detect any more of them nearby, though."translation="لم أعد أستطيع استكشاف المزيد منه على مقربة منا."/>
<dialoguespeaker="yellow"english="Maybe you've found them all?"translation="ربما يعني هذا أنك وجدتهم كلهم؟"/>
<dialoguespeaker="cyan"english="Aha! This must be what's causing the interference!"translation="آ-هاه! هذا مسبب التشويش بلا شك!"/>
<dialoguespeaker="cyan"english="I wonder if I can turn it off?"translation="هل يمكنني إطفاؤه يا ترى؟"/>
<dialoguespeaker="gray"english="WARNING: Disabling the Dimensional Stability Generator may lead to instability! Are you sure you want to do this?"translation="تحذير: تعطيل عمل فارض استقرار البعد قد يؤدي لحصول اختلال في الاستقرار! أواثق أن هذه نيتك حقا؟
<dialoguespeaker="gray"english="Seriously! The whole dimension could collapse! Just think about this for a minute!
Are you really sure you want to do this?"translation="ويحك أرجوك! قد يودي هذا بهذا البعد برمته إلى الانهيار الشامل! هلا تعقلت؟ هلا فكرت في التبعات ببعض الجدية، ولو لبرهة؟ ما هو قرارك؟
<dialoguespeaker="blue"english="I knew you'd be ok!"translation="كنت على ثقة بنجاتك!"/>
<dialoguespeaker="purple"english="We were very worried when you didn't come back..."translation="قلقنا كثيرا عندما تأخرت عودتك..."/>
<dialoguespeaker="green"english="...but when you turned off the source of the interference..."translation="...لكن عندما أطفأت أنت مصدر التشويش..."/>
<dialoguespeaker="yellow"english="...we were able to find you with the ship's scanners..."translation="...تمكننا براصدات السفينة من تحديد مكانك..."/>
<dialoguespeaker="red"english="...and teleport you back on board!"translation="...وقمنا بتنقيلك على متن السفينة!"/>
<dialoguespeaker="player"english="That was lucky!"translation="ذلك من حسن حظي!"/>
<dialoguespeaker="player"english="Thanks guys!"translation="شكرا يا جماعة!"/>
<dialoguespeaker="yellow"english="...it looks like this dimension is starting to destabilise, just like our own..."translation="...يبدو أن استقرار هذا البعد يختل، مثل ما حصل مع بعدنا..."/>
<dialoguespeaker="red"english="...we can stay and explore for a little longer, but..."translation="...يمكننا أن نبقى قليلا بعد هنا لاستطلاع المكان..."/>
<dialoguespeaker="yellow"english="...eventually, it'll collapse completely."translation="...لكن ستأتي لحظة ينهار فيها هذا البعد برمته."/>
<dialoguespeaker="green"english="There's no telling exactly how long we have here. But the ship's fixed, so..."translation="لا سبيل لنحدد بالضبط كم المهلة الزمنية المتبقية لنا هنا. لكن سفينتنا أصلحت على كل حال..."/>
<dialoguespeaker="blue"english="...as soon as we're ready, we can go home!"translation="...لذا، فور أن نستعد، نستطيع العودة لديارنا!"/>
<dialoguespeaker="purple"english="What now, Captain?"translation="والآن يا قبطان، ماذا نحن فاعلون؟"/>
<dialoguespeaker="player"english="Let's find a way to save this dimension!"translation="فلنبحث عن طريقة لإنقاذ هذا البعد!"/>
<dialoguespeaker="player"english="And a way to save our home dimension too!"translation="وكذلك... عن طريقة لإنقاذ بعدنا الأصلي!"/>
<dialoguespeaker="player"english="The answer is out there, somewhere!"translation="الجواب ينتظرنا في مكان ما!"/>
<dialoguespeaker="blue"english="Wow! You found all of them!"translation="واو! وجدتها كلها!"/>
<dialoguespeaker="player"english="Really? Great!"translation="صحيح؟ هذا خبر عظيم!"/>
<dialoguespeaker="blue"english="I'll run some tests and see if I can work out what they're for..."translation="سأجري بعض الاختبارات كي أحدد ما فائدتها..."/>
<dialoguespeaker="player"english="That... that didn't sound good..."translation="ليس... ليس هذا مطمئنا..."/>
<dialoguespeaker="red"english="Not again!"translation="آه ليس مجددا!"/>
<dialoguespeaker="player"english="Wait! It's stopped!"translation="صبرا! توقف الصوت!"/>
<dialoguespeaker="purple"english="This is where we were storing those shiny things? What happened?"translation="هنا كنا نخزن تلك الأغراض اللماعة؟ ماذا حصل لها؟"/>
<dialoguespeaker="player"english="We were just playing with them, and..."translation="كنا نعبث بها فحسب، وإذ بها..."/>
<dialoguespeaker="purple"english="Or, you know... we could have just warped back to the ship..."translation="هل تعلمون... عوض هذا، كنا نستطيع التنقل للسفينة..."/>
<dialoguespeaker="green"english="Wow! What is this?"translation="واو! ما هذا؟"/>
<dialoguespeaker="yellow"english="It looks like another laboratory!"translation="كأنه مخبر آخر!"/>
<dialoguespeaker="red"english="Let's have a look around!"translation="فلنلق نظرة في الأنحاء!"/>
</cutscene>
<cutsceneid="talkpurple_9"explanation="">
<dialoguespeaker="purple"english="Look at all this research! This is going to be a big help back home!"translation="يا سلام، هل رأيت كل هذه الأبحاث؟ ستفيدنا فائدة جمة لو رجعنا بها للوطن!"/>
</cutscene>
<cutsceneid="talkgreen_11"explanation="">
<dialoguespeaker="green"english="I wonder why they abandoned this dimension? They were so close to working out how to fix it..."translation="يا ترى لماذا هجروا هذا البعد؟ مع أنهم أوشكوا على اكتشاف الحل لإصلاحه..."/>
<dialoguespeaker="green"english="Maybe we can fix it for them? Maybe they'll come back?"translation="ربما نصلحه نيابة عنهم؟ ربما يعودون بعد ذلك؟"/>
</cutscene>
<cutsceneid="talkblue_9"explanation="">
<dialoguespeaker="blue"english="This lab is amazing! The scientists who worked here know a lot more about warp technology than we do!"translation="مخبر رائع! علماؤه يعرفون أكثر منا بكثير عن تقنيات علوم التنقيل المكاني!"/>
</cutscene>
<cutsceneid="talkyellow_12"explanation="">
<dialoguespeaker="yellow"english="Captain! Have you seen this?"translation="قبطان! هل سبق ورأيت هذا؟"/>
<dialoguespeaker="yellow"english="With their research and ours, we should be able to stabilise our own dimension!"translation="لو جمعنا بين أبحاثهم وأبحاثنا، سنتوصل للحل الذي يحقق استقرار بعد أوطاننا!"/>
<dialoguespeaker="red"english="Look what I found!"translation="شاهدوا ماذا وجدت!"/>
<dialoguespeaker="red"english="It's pretty hard, I can only last for about 10 seconds..."translation="اللعبة صعبة جدا، لم أستطع الصمود أكثر من 10 ثوان..."/>
</cutscene>
<cutsceneid="terminal_jukebox"explanation="">
<dialoguespeaker="gray"english="-= JUKEBOX =-
Songs will continue to play until you leave the ship.
Collect trinkets to unlock new songs!"translation="-= مشغل الموسيقى =-
يستمر عزف الأغنية حتى مغادرة السفينة.
كلما جمعت تذكارات، فتحت أغان جديدة!"centertext="1"padtowidth="264"/>
</cutscene>
<cutsceneid="terminal_jukeunlock1"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
5 Trinkets
Pushing Onwards"translation="الشرط: 5 تذكارات
يفتح الأغنية القادمة
Pushing Onwards"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock2"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
8 Trinkets
Positive Force"translation="الشرط: 8 تذكارات
يفتح الأغنية القادمة
Positive Force"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock3"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
10 Trinkets
Presenting VVVVVV"translation="الشرط: 10 تذكارات
يفتح الأغنية القادمة
Presenting VVVVVV"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock4"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
12 Trinkets
Potential for Anything"translation="الشرط: 12 تذكار
<dialoguespeaker="gray"english="-= PERSONAL LOG =-"translation="-= مذكرات شخصية =-"padtowidth="280"/>
<dialoguespeaker="gray"english="Almost everyone has been evacuated from the space station now. The rest of us are leaving in a couple of days, once our research has been completed."translation="أخلينا الجميع من المحطة الفضائية. سيغادر من تبقى منا بعد يومين، عقب انتهاء أبحاثنا. ؝"pad="1"/>
</cutscene>
<cutsceneid="terminal_station_2"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...everything collapses, eventually. It's the way of the universe."translation="...كل شيء سائر إلى الانهيار، طال الزمان أو قصر. هكذا سنة الحياة في الكون. ؝"centertext="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_station_3"explanation="">
<dialoguespeaker="gray"english="I wonder if the generator we set up in the polar dimension is what's affecting our teleporters?"translation="أتساءل... هل للمولد الذي شغلناه في البعد القطبي دخل في التأثير الطارئ على آلات تنقيلنا؟"/>
<dialoguespeaker="gray"english="No, it's probably just a glitch."translation="لا أظن، ذلك مجرد عيب تقني خفيف على الأرجح. ؝"/>
</cutscene>
<cutsceneid="terminal_station_4"explanation="a trinket that's difficult to get">
<dialoguespeaker="gray"english="-= PERSONAL LOG =-"translation="-= مذكرات شخصية =-"padtowidth="280"/>
<dialoguespeaker="gray"english="Hah! Nobody will ever get this one."translation="هاع! أتحدى أي مخلوق أن يفوز بهذه بالذات. ؝"pad="1"/>
</cutscene>
<cutsceneid="terminal_warp_1"explanation="">
<dialoguespeaker="gray"english="...The other day I was chased down a hallway by a giant cube with the word AVOID on it."translation="... قبل كم يوم، طاردني في أحد الأروقة مكعب عملاق يتدحرج وعليه كلمة [تفادى]."/>
<dialoguespeaker="gray"english="These security measures go too far!"translation="بالغوا حقا في إجراءات الحماية!! ؝"/>
</cutscene>
<cutsceneid="terminal_warp_2"explanation="">
<dialoguespeaker="gray"english="The only way into my private lab anymore is by teleporter."translation="لم تبق أي طريقة لدخول مخبري الخاص سوى آلة التنقيل."/>
<dialoguespeaker="gray"english="I've made sure that it's difficult for unauthorised personnel to gain access."translation="حرصت أن أصعب الدخول على غير العمال المسموح لهم. ؝"/>
</cutscene>
<cutsceneid="terminal_outside_1"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... our first breakthrough was the creation of the inversion plane, which creates a mirrored dimension beyond a given event horizon ..."translation="... إنجازنا وسبقنا الأول بدأ من إنشاء مستوي التعاكس، مما من شأنه إنشاء بعد معكوس جديد عند زمكان ما بعد نقطة أفق حدث معروفة في معطياتنا ..."pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_2"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...with just a small modification to the usual parameters, we were able to stabilise an infinite tunnel!"translation="...بإدخال تعديل بسيط على المتغيرات المعتادة، نجحنا في الحفاظ على استقرار نفق لا متناه! ؝"/>
</cutscene>
<cutsceneid="terminal_outside_3"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... the final step in creating the dimensional stabiliser was to create a feedback loop ..."translation="... وآخر خطوة لإنشاء فارض استقرار البعد أن نصنع حلقة ارتجاع ..."pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_4"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...despite our best efforts, the dimensional stabiliser won't hold out forever. Its collapse is inevitable..."translation="... مهما بذلنا من جهود، لن يتحمل فارض توازن البعد إلى ما لا نهاية. انهياره محتوم عاجلا أم آجلا..."pad="1"/>
<dialoguespeaker="cyan"english="Huh? These coordinates aren't even in this dimension!"translation="عجبا؟ هذه الإحداثيات خارجة عن نطاق هذا البعد!"/>
</cutscene>
<cutsceneid="terminal_outside_5"explanation="">
<dialoguespeaker="gray"english="-= Personal Log =-"translation="-= مذكرات شخصية =-"padtowidth="232"/>
<dialoguespeaker="gray"english="... I've had to seal off access to most of our research. Who knows what could happen if it fell into the wrong hands? ..."translation="... اضطررت لتعطيل الدخول إلى معظم أبحاثنا. من يدري ماذا قد يحصل لو وقعت في أيادي السوء؟ ..."centertext="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_6"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= ملاحظات الباحثين =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... access to the control center is still possible through the main atmospheric filters ..."translation="... الدخول إلى مركز التحكم لا يزال ممكنا لو كان عبر مصافي الأغلفة الجوية الأساسية ..."/>
</cutscene>
<cutsceneid="terminal_lab_1"explanation="">
<dialoguespeaker="gray"english="... it turns out the key to stabilising this dimension was to create a balancing force outside of it!"translation="... اتضح أن الحل لاستقرار هذا البعد يكمن في إنشاء قوة خارجه لإحلال التوازن!"/>
<dialoguespeaker="gray"english="Though it looks like that's just a temporary solution, at best."translation="إلا أن هذا مجرد حل وقتي، ولن يدوم حتى في أحسن الظروف."/>
<dialoguespeaker="gray"english="I've been working on something more permanent, but it seems it's going to be too late..."translation="حاولت العمل على حل دائم يرسخ مفعوله، لكن يبدو لي أن الأوان سيفوت قبل تطبيقه. ؝"/>
<dialoguespeaker="player"english="Now that the ship is fixed, we can leave anytime we want!"translation="بعد أن أصلحنا السفينة، يمكننا المغادرة متى شئنا!"/>
<dialoguespeaker="player"english="We've all agreed to keep exploring this dimension, though."translation="لكننا اتفقنا جميعا أن نواصل استكشاف هذا البعد."/>
<dialoguespeaker="player"english="Who knows what we'll find?"translation="من يدري ماذا قد نجد؟"/>
</cutscene>
<cutsceneid="terminal_radio"explanation="">
<dialoguespeaker="gray"english="-= SHIP RADIO =-
[ Status ]
Broadcasting"translation="-= راديو السفينة =-
[ الوضع الحالي ]
الإرسال قيد البث"tt="1"centertext="1"pad="2"/>
</cutscene>
<cutsceneid="terminal_secretlab"explanation="">
<dialoguespeaker="gray"english="-= WARNING =-
The Super-Gravitron is intended for entertainment purposes only."translation="-= تحذير =-
<dialoguespeaker="gray"english="Anyone found using the Super Gravitron for educational purposes may be asked to stand in the naughty corner."translation="لو افتضح أمر شخص يستخدمه لأغراض تعليمية، سنطلب منه أن يبكي في زاوية العقاب."/>
<dialoguespeaker="cyan"english="...oh, I've already found this."translation="...أوه، سبق أن وجدته."/>
</cutscene>
<cutsceneid="disableaccessibility"explanation="">
<dialoguespeaker="gray"english="Please disable invincibility and/or slowdown before entering the Super Gravitron."translation="يرجى تعطيل الحصانة وتبطئة اللعبة قبل دخول غرفة أتون الجاذبية الخارق."/>
<!-- You can translate these in-game to get better context! See README.txt -->
<roomnames>
<roomnamex="0"y="18"english="Single-slit Experiment"translation="التجربة 1: ثغرة مفردة"explanation="(Many of the rooms in the Lab stage have science themed names.)"/>
<roomnamex="0"y="19"english="Don't Flip Out"translation="لا ينقلبن مزاجك"explanation="Flip as in gravity flip, but also the expression in english, as in, keep your cool"/>
<roomnamex="1"y="0"english="I'm Sorry"translation="آسف"explanation="The room below this one is Please Forgive Me. There is also a secret path to the right which leads to the rooms 'Anomaly' and 'Purest Unobtainium'."/>
<roomnamex="1"y="1"english="Please Forgive Me!"translation="سامحني أرجوك!"explanation="The room above this one is I'm Sorry"/>
<roomnamex="1"y="17"english="Rascasse"translation="الأسماك الشائكة"explanation="This is a type of fish with lots of thorny spikes on its back"/>
<roomnamex="1"y="18"english="Keep Going"translation="نواصل"explanation="Literally just 'Keep Going', through the room"/>
<roomnamex="1"y="19"english="Shuffled Hallway"translation="رواق منحاز"explanation="Just describes how the room looks. 'Shuffled' as in offset."/>
<roomnamex="2"y="0"english="Kids His Age Bounce"translation="الصبيان في عمره ينطون"explanation="An old saying for when e.g. a small child falls out of a tree. A trampoline joke in this case."/>
<roomnamex="2"y="1"english="Playing Foosball"translation="كرة قدم الطاولة"explanation="This room resembles a Foosball table - https://en.wikipedia.org/wiki/Table_football"/>
<roomnamex="2"y="4"english="Philadelphia Experiment"translation="تجربة فيلاديلفيا"explanation="There is a teleporter in this room. The Philadelphia Experiment is the name of 80's film about teleportation."/>
<roomnamex="2"y="16"english="Get Ready To Bounce"translation="نستعد لننط"explanation="The first room in the Lab zone. In the next room, you immediately come into contact with a gravity line, and flip back up."/>
<roomnamex="2"y="17"english="It's Perfectly Safe"translation="كله أمان فعليك بالاطمئنان"explanation="The second room in the lab zone. Don't worry, it's perfectly safe."/>
<roomnamex="2"y="18"english="Young Man, It's Worth the Challenge"translation="بني، التحدي يستحق المجهود"explanation="Not a reference to anything in particular - Bennett explains that this is just something that his high school chemistry teacher used to say to students. He thinks the teacher was probably misquoting George Bernard Shaw, who said 'Life is not meant to be easy, my child - but take courage: it can be delightful.'."/>
<roomnamex="2"y="19"english="Double-slit Experiment"translation="التجربة 2: ثغرة مثناة"explanation="(Many of the rooms in the Lab stage have science themed names.)"/>
<roomnamex="3"y="0"english="Merge"translation="التحام"explanation="A wide section connecting to a narrower section - merge as in traffic"/>
<roomnamex="3"y="1"english="A Difficult Chord"translation="عزفت على الوتر الحساس"explanation="This room resembles a guitar chord"/>
<roomnamex="3"y="4"english="Why So Blue?"translation="يا نهار أزرق! لماذا الحزن؟"explanation="The crewmate Victoria is found here. Victoria is always sad! She's feeling blue."/>
<roomnamex="3"y="16"english="Brought to you by the letter G"translation="برعاية الحرف G"explanation="Reference to Seseme Street - also, this room resembles the letter G"/>
<roomnamex="3"y="17"english="Thorny Exchange"translation="تراشق إبر الكلام"explanation="Two people having a polite argument could be described as having a Thorny Exchange of words."/>
<roomnamex="3"y="18"english="Square Root"translation="الجذر التربيعي"explanation="This room resembles a Square Root symbol."/>
<roomnamex="3"y="19"english="They Call Him Flipper"translation="صديق الإنسان، شقلوب"explanation="This is a line from an American TV show intro about a Dolphin - https://www.youtube.com/watch?v=azEOeTX1LqM"/>
<roomnamex="4"y="0"english="Vibrating String Problem"translation="إشكالية الوتر المتذبذب"explanation="Another science themed room name, this one has two gravity lines that you bounce between."/>
<roomnamex="4"y="1"english="The Living Dead End"translation="الطرف المستطرف المتطرف"explanation="'The living end' is an idiom meaning 'the most extreme form of something', here it's an extreme dead end, i.e. a cul-de-sac"/>
<roomnamex="4"y="2"english="AAAAAA"translation="آآآآآآ"explanation="The player falls through this room without having time to stop - AAAAAA suggests a scream in English. Also, it's six As, like the title."/>
<roomnamex="4"y="3"english="Diode"translation="صمام"explanation="This room can only be passed through in one direction. It resembles the electrical component."/>
<roomnamex="4"y="4"english="I Smell Ozone"translation="شممت الأوزون"explanation="When you use a photocopier, it produces a distinctive smell (from the Ozone produced). This room has a background pattern that suggests a teleportation has recently happened here."/>
<roomnamex="4"y="16"english="Free Your Mind"translation="حرر دماغك"explanation="Reference to the film The Matrix, where Morpheus jumps from the top of a Skyscaper - https://www.youtube.com/watch?v=ef_agVIvh0A"/>
<roomnamex="4"y="17"english="I Changed My Mind, Thelma..."translation="بدلت رأيي يا ثيلما..."explanation="The room below Free Your Mind. Reference to the film Thelma and Louise, which ends with Thelma and Louise driving off a cliff, sorry, spoilers"/>
<roomnamex="4"y="18"english="Hitting the Apex"translation="ذروة المنعطف"explanation="'Hitting the Apex' is the term used by race drivers for the optimal path around a corner"/>
<roomnamex="4"y="19"english="Three's a Crowd"translation="سر الثلاثة سر الجمهرة"explanation="From the expression Two's Company, Three's a Crowd. This room has two challenges - the first has two gaps to cross, the second has three."/>
<roomnamex="5"y="0"english="Spike Strip Deployed"translation="فلتنشر الأشواك"explanation="This room has some spikes on a gravity line. The name is a reference to the device that police might use to blow out the tyres of a speeding car."/>
<roomnamex="5"y="1"english="Anomaly"translation="الظاهرة الغريبة"explanation="As in, a strange result in science. This room has lots of different colours, unlike other rooms in this stage."/>
<roomnamex="5"y="16"english="In a Single Bound"translation="بوثبة واحدة"explanation="Superman is described as being able to leap tall buildings in a single bound."/>
<roomnamex="5"y="17"english="Indirect Jump Vector"translation="متجه قفزة غير مباشرة"explanation="If you miss the gap in 'In a Single Bound' above, you will end up back in this room - hence, your trajectory was off!"/>
<roomnamex="6"y="0"english="Topsy Turvyism"translation="عاليها سافلها"explanation="Topsy Turvy is Australian slang for upside down"/>
<roomnamex="6"y="1"english="Purest Unobtainium"translation="معدن المجهولنديوم الخالص"explanation="Unobtainium is a jokey made up term from science fiction for an impossible substance - https://en.wikipedia.org/wiki/Unobtainium"/>
<roomnamex="6"y="16"english="Barani, Barani"translation="شقلبة، شقلبتان"explanation="A Barani is a technical term for doing a flip on a trampoline"/>
<roomnamex="6"y="17"english="Safety Dance"translation="رقصة الأمان"explanation="Named after the 80s song by Men Without Hats."/>
<roomnamex="7"y="0"english="Standing Wave"translation="الموجة الموقوفة"explanation="Many of the rooms in the Lab stage have science themed names. This one is at the beginning of a section with gravity lines above and below you, before the section begins."/>
<roomnamex="7"y="15"english="Entanglement Generator"translation="كم تشابكا ستولد بآلتك؟"explanation="This room contains a teleporter. Entanglement is an idea from quantum mechanics."/>
<roomnamex="7"y="16"english="Heady Heights"translation="الرأس الأعلى"explanation="Just below the highest point in the level."/>
<roomnamex="7"y="17"english="Exhausted?"translation="النشاط العادم"explanation="This room has an exit that sort of suggests an Exhaust Pipe in a car."/>
<roomnamex="7"y="18"english="The Tantalizing Trinket"translation="سأتذكر التذكار"explanation="You see this Trinket just out of reach as you fall through the room."/>
<roomnamex="7"y="19"english="The Bernoulli Principle"translation="مبدأ بيرنولي"explanation="Many of the rooms in the Lab stage have science themed names - this one is just named after a formula relating to flight."/>
<roomnamex="8"y="9"english="Teleporter Divot"translation="أثر التنقيلة"explanation="There is a pattern in the background of this room that indicates that a teleporter has sent someone to this room"/>
<roomnamex="9"y="9"english="The Tower"translation="البرج"explanation="This room is a single vertically scrolling stage, about 20 rooms high. The name is from the Tarot card."/>
<roomnamex="10"y="4"english="Seeing Red"translation="العين الحمراء"explanation="This is the room that you find the red crewmate in. (Seeing Red is an expression in English about being filled with rage, but that doesn't really apply here)"/>
<roomnamex="10"y="5"english="Energize"translation="تنشيط"explanation="There is a teleporter in this Room. Energize is what they say on Star Trek when they use the teleporters."/>
<roomnamex="10"y="6"english="Down Under"translation="تحت أم الدنيا في آخر الدنيا"explanation="Australia is sometimes called The Land Down Under because of its position on a globe. You complete this room by going down and under some moving platforms"/>
<roomnamex="10"y="7"english="A Deception"translation="أخدوعة"explanation="This room appears trivial at first, but is connected to a difficult trinket challenge"/>
<roomnamex="11"y="4"english="Building Apport"translation="توطيد الروابط الزمكانية"explanation="An 'Apport' is a kind of paranormal teleportation. Building Apport is a pun on the concept of 'building rapport' (except that to 'apport' is to teleport)."/>
<roomnamex="11"y="5"english="Frown Upside Down"translation="اقلب عبوس حواجبك"explanation="To 'Turn that Frown Upside Down' is an expression in English, like 'cheer up', basically means to stop being unhappy"/>
<roomnamex="11"y="6"english="Shenanigan"translation="ألعوبة"explanation="Shenanigan as in a prank, or practical joke. This room is connected to the 'Prize for the Reckless' puzzle, and like the room 'A Deception', it appears trivial unless you know the secret"/>
<roomnamex="11"y="7"english="Prize for the Reckless"translation="جائزة للمتهورين"explanation="This room contains a trinket that can only be collected by doing something difficult"/>
<roomnamex="11"y="11"english="Conveying a New Idea"translation="توصيل الفكرة ببساط-ة"explanation="This is the first room you encounter that has conveyor belts in it"/>
<roomnamex="11"y="12"english="One Way Room"translation="دخول الحمام ليس كخروجه"explanation="Can only be travelled through in one direction"/>
<roomnamex="11"y="13"english="Boldly To Go"translation="بجرأة فلنمض"explanation="Star Trek reference, this rooms is near the entrance to the space station level"/>
<roomnamex="11"y="14"english="The Filter"translation="الغربال"explanation="Like a filter from an air conditioning vent"/>
<roomnamex="12"y="3"english="Security Sweep"translation="تمشيط أمني"explanation="Contains a single, fast moving enemy that moves up and down."/>
<roomnamex="12"y="4"english="Gantry and Dolly"translation="الرافعة الجسرية"explanation="Gantry and Dolly are the names for types of cranes that move crates around. This room has two different types of platforms."/>
<roomnamex="12"y="5"english="The Yes Men"translation="عشاق كلمة نعم"explanation="Contains a number of enemies with briefcases and the word -YES- for a head. An expression for people who work at large companies and agree a lot with their bosses"/>
<roomnamex="12"y="6"english="Stop and Reflect"translation="وقفة تأمل"explanation="Expression meaning to take a moment and think about what you're doing. In this room, a small puzzle where you need to use the underside of a moving platform to progress."/>
<roomnamex="12"y="7"english="V Stitch"translation="غرزة مثلثة"explanation="A V Stitch is a type of crochet stitch."/>
<roomnamex="12"y="11"english="Upstream Downstream"translation="مع التيار، عكس التيار"explanation="As in swimming upstream or downstream, with or against a current in a river"/>
<roomnamex="12"y="12"english="The High Road is Low"translation="أعلى الطريقين مقاما أدناهما"explanation="This room has two paths - a high path and a low path. The 'low' path leads to a trinket, so the roomname is a sort of clue about which way to go."/>
<roomnamex="12"y="13"english="Give Me A V"translation="صفحة! سبعة! صعبة!"explanation="Room is in the shape of a big letter V. The roomname suggests the common american cheerleading chant - e.g. Give me an L! Give me an O! Give me a C! Give me an A! Give me an L! Give me an I! Give me an S! Give me an A! Give me a T! Give me an I! Give me an O! Give me an N! What does it spell? LOCALISATION!"/>
<roomnamex="12"y="14"english="Outer Hull"translation="المتن الخارجي"explanation="The entrance to the Space Station 2 level - the outer hull of a space station."/>
<roomnamex="13"y="0"english="It's Not Easy Being Green"translation="الخضرة صعبة"explanation="References a song by Kermit from the Muppets. This room is where you find the green crewmate."/>
<roomnamex="13"y="3"english="Linear Collider"translation="المصادم الخطي"explanation="An early room, name is just meant to suggest something sciency. Room contains long, wave like enemies."/>
<roomnamex="13"y="4"english="Comms Relay"translation="ناقل البث"explanation="This room contains some communication equipment, like a radio."/>
<roomnamex="13"y="5"english="Welcome Aboard"translation="مرحبا بك"explanation="The first room in the game"/>
<roomnamex="13"y="6"english="Trench Warfare"translation="حرب الخنادق"explanation="Room contains a couple of pits with soldier-like enemies in them. Loosely references the 1983 videogame Hunchback."/>
<roomnamex="13"y="7"english="B-B-B-Busted"translation="هههذه المصيدة حافلة"explanation="Room contains a large Bus. 'Bus'ted as in 'Caught'."/>
<roomnamex="13"y="8"english="Level Complete!"translation="ختمت المستوى!"explanation="This room has a teleporter, which is normally found at the end of a level. However this room is midway through the stage."/>
<roomnamex="13"y="9"english="Lighter Than Air"translation="أخف من الهواء"explanation="This room has clouds that rise from the bottom of the screen to the top, which the player is faster than, implying that the player is lighter than air."/>
<roomnamex="13"y="10"english="The Solution is Dilution"translation="الحل في الانحلال"explanation="This room has a factory and pollution clouds in it. Apparently this phrase was once used by industrialists to advocate for not worrying too much about pollution."/>
<roomnamex="13"y="11"english="The Cuckoo"translation="كوكو الكذوب"explanation="This room contains a speaker that emits the word 'LIES' over and over. A cuckoo's call decieves other birds!"/>
<roomnamex="13"y="12"english="Backsliders"translation="بساط الريح، بساط الرجع"explanation="A conveyor belt in this room pushes against you as you try to move, so you slide backwards."/>
<roomnamex="13"y="13"english="Select Track"translation="مساران ولك الخيار"explanation="There are two paths you can pick between here"/>
<roomnamex="14"y="0"english="Green Dudes Can't Flip"translation="الخضر لا يعكسون"explanation="You have a green crewmate with you in this room! A reference to the 90's film 'White Guy's Can't Jump'."/>
<roomnamex="14"y="1"english="This is how it is"translation="هكذا الحال"explanation="literally as in, this is how the mechanic of this stage works - also an expression as in 'this is the way things are'"/>
<roomnamex="14"y="2"english="That's Why I Have To Kill You"translation="لهذا سأضطر لقتلك"explanation="The follows the room named 'I love you'. 'I love you, that's why I have to kill you' is kind of a slasher horror trope."/>
<roomnamex="14"y="3"english="Atmospheric Filtering Unit"translation="وحدة المصفاة الجوية"explanation="An early room, looks a bit like an air filter"/>
<roomnamex="14"y="4"english="It's a Secret to Nobody"translation="سر مفضوح للجميع"explanation="A reference to the infamous Zelda quote 'It's a secret to everybody'. This room contains the first trinket."/>
<roomnamex="14"y="5"english="Conundrum"translation="حيرة"explanation="Conundrum as in puzzle, riddle, problem to be solved"/>
<roomnamex="14"y="6"english="Boo! Think Fast!"translation="بعو! بسرعة فلنفكر!"explanation="Contains a challenge that you need to react very quickly to. You might say 'Boo, think fast' if you threw something at someone, expecting them to catch it."/>
<roomnamex="14"y="7"english="The Sensible Room"translation="الغرفة العقلانية"explanation="Early corridor room containing no challenges. Sensible as in the opposite of Foolish - you might call someone sensible in english if they are excessively cautious."/>
<roomnamex="14"y="8"english="The Hanged Man, Reversed"translation="مشنوق، لكن بالعكس"explanation="Named after the Tarot Card, reversed as in Upside Down. The room contains a stationary enemy which resembles a Wheel of Fortune. The name is supposed to suggest a kind of out-of-place quality."/>
<roomnamex="14"y="9"english="Green Grotto"translation="المغارة الخضراء"explanation="A peaceful green room."/>
<roomnamex="14"y="10"english="Manic Mine"translation="المنجم المجنون"explanation="A reference to the 8-bit game Manic Miner."/>
<roomnamex="14"y="11"english="Clarion Call"translation="نداء الكاذب"explanation="A 'Clarion Call' is an idiom used when somebody makes a case for a course of action, for example in a politician's speech, or a call to battle. It sometimes has an association with dishonesty - in this room, the words 'LIES' appear over and over."/>
<roomnamex="14"y="12"english="Gordian Knot"translation="المعقودة الغوردية"explanation="As in the Gordian Knot from greek history. A complicated room that can be passed through twice."/>
<roomnamex="14"y="13"english="You Chose... Poorly"translation="اختيارك... لم يكن في محله"explanation="This room comes right after a choice between two paths. It's a quote from an Indiana Jones film."/>
<roomnamex="15"y="0"english="Murdering Twinmaker"translation="آلة التوأمة الذباحة"explanation="Room contains a teleporter. A 'Murdering Twinmaker' is, uh, one way teleportation might work..."/>
<roomnamex="15"y="1"english="A Bisected Spiral"translation="دوامة مشطورة"explanation="Room is a spiral, cut down the middle"/>
<roomnamex="15"y="2"english="Take the Red Pill"translation="عليك بالحبة الحمراء"explanation="This is a Matrix reference that hasn't aged well, lol"/>
<roomnamex="15"y="3"english="Traffic Jam"translation="زحمة مرور"explanation="The enemies in this room are Stop Signs"/>
<roomnamex="15"y="4"english="Leap of Faith"translation="نتوكل ونقفز"explanation="To take a leap of faith means to do something without knowing how it's going to turn out."/>
<roomnamex="15"y="5"english="Solitude"translation="عزلة"explanation="As in being alone, or in this case, lost by yourself"/>
<roomnamex="15"y="6"english="Driller"translation="الكابتن حفار"explanation="Technically references the name of a C64 game, but that doesn't matter much"/>
<roomnamex="15"y="7"english="Exhaust Chute"translation="مسقط النفايات"explanation="Like a factory exhaust chute for disposing of rubbish"/>
<roomnamex="15"y="8"english="Sorrow"translation="يا للأحزان"explanation="A difficult room that you might die in a lot"/>
<roomnamex="15"y="9"english="doomS"translation="قبر حرب في rbb nlSo"explanation="The room is above 'Swoop', and is a copy of the room rotated 180 degrees! The room name 'doomS' is the word 'Swoop' rotated 180 degrees. When localising this room, don't worry too much about trying to keep the meaning of the words Dooms and Swoop, because they're not that important - instead, focus on picking words that have this 180 degree flip quality!"/>
<roomnamex="15"y="10"english="Swoop"translation="nrc rib في مكان قفر"explanation="See the note for room (15,9), doomS."/>
<roomnamex="15"y="11"english="Chinese Rooms"translation="الغرف الصينية"explanation="This refers to a famous philosophical argument about artifical intelligence - https://en.wikipedia.org/wiki/Chinese_room."/>
<roomnamex="15"y="12"english="You Just Keep Coming Back"translation="ستغيب، ستروح، وسترجع يا حياتي"explanation="You can pass through this room up to three times, depending on which route you take through the level."/>
<roomnamex="15"y="13"english="Hyperspace Bypass 5"translation="متخطى الفضاء الفائق رقم 5"explanation="A conveyor belt will take you through this room without you needing to press any buttons, hench the bypass. The phrase Hyperspace Bypass is a reference to Hitchhiker's Guide to the Galaxy."/>
<roomnamex="16"y="0"english="I Love You"translation="أحبك"explanation="The room contains a couple of heart shaped enemies"/>
<roomnamex="16"y="1"english="As you like it"translation="كما شئت"explanation="This room can be approached in two different equivilent ways, whichever way you like it. 'As you like it' is the name of a Shakespeare play."/>
<roomnamex="16"y="2"english="Short Circuit"translation="دارة مقصورة"explanation="Probably named after the 80s film Short Circuit. Also works because you'll hit a dead end if you keep walking forwards."/>
<roomnamex="16"y="3"english="Twisty Little Passages"translation="طرق تقلصت وتشعبت"explanation="A maze like room. Refers to the section from the 1976 text game Colossal Cave Adventure - you are in a maze of twisty little passages, all alike"/>
<roomnamex="16"y="6"english="Quicksand"translation="الرمال المتحركة"explanation="Contains lots of dissolving platforms."/>
<roomnamex="16"y="7"english="The Tomb of Mad Carew"translation="قبر كارو المجنون"explanation="A very obscure reference to the C64 game Dizzy"/>
<roomnamex="16"y="8"english="Parabolica"translation="العدسة الشلجمية"explanation="This room contains a section of wall in the shape of a parabolic arch."/>
<roomnamex="16"y="9"english="$eeing Dollar $ign$"translation="أرى ﺷ$ﻞ دولار"explanation="This is a green room that resembles a dollar sign shape"/>
<roomnamex="16"y="10"english="What Lies Beneath?"translation="تحت الأكاذيب؟"explanation="The room below this contains enemies in the shape of the word 'Lies'. So there's a double meaning here - as in, a question, 'what is below this room', and that the word LIES is literally beneath this room."/>
<roomnamex="16"y="11"english="Spikes Do!"translation="تحصد الأشواك!"explanation="This rooms is below the room named 'What lies Beneath?', and answers the question: Spikes do!"/>
<roomnamex="16"y="12"english="Ha Ha Ha Not Really"translation="هههههه صدقتني"explanation="This is a difficult room that follows one called 'Plain Sailing from here on'. It's taunting the player. "/>
<roomnamex="16"y="13"english="Plain Sailing from Here On"translation="طريق سالكة من الآن فصاعدا"explanation="This room is at the end of a long section, and promises 'Plain Sailing' afterwards, as in, no further challenges. This is a lie"/>
<roomnamex="17"y="0"english="As we go up, we go down"translation="صعودنا سقوط"explanation="Named after the 1995 song by Guided by Voices"/>
<roomnamex="17"y="1"english="Maze With No Entrance"translation="متاهة بلا مدخل"explanation="This room is a maze which has no entrance, due to the nature of the warping mechanic."/>
<roomnamex="17"y="2"english="The Brown Gate"translation="البوابة البنية"explanation="I think this is an Ultima 7 reference? A literal translation is fine here"/>
<roomnamex="17"y="3"english="Edge Games"translation="نلعب على حافة الخطر"explanation="Contains a trinket that you get by navigating around the edge of the screen. The use of the word EDGE is deliberate, refering to the trademark of a notoriously litigious individual who sued an indie developer around the time VVVVVV was made"/>
<roomnamex="17"y="7"english="Brass Sent Us Under The Top"translation="كتيبة المتسلقين"explanation="The enemies in this room look like little army guys"/>
<roomnamex="17"y="8"english="The Warning"translation="من أنذر فقد أعذر"explanation="This room has lots of checkpoints in it. It's beside the game's most difficult challenge, the Veni, Vidi, Vici section. The checkpoints don't really do anything, but they're a warning of the challenge ahead."/>
<roomnamex="17"y="9"english="Just Pick Yourself Down"translation="التقط نفسك من تحت"explanation="A two part room name. From the expression 'If you fall down, just pick yourself up'."/>
<roomnamex="17"y="10"english="If You Fall Up"translation="لو سقطت إلى فوق"explanation="A two part room name. From the expression 'If you fall down, just pick yourself up'."/>
<roomnamex="17"y="11"english="Chipper Cipher"translation="شفرة بهجة"explanation="Just some nice wordplay. Chipper means 'Jolly' or 'Happy'."/>
<roomnamex="18"y="0"english="Time to get serious"translation="حان وقت الجد"explanation="Literal, this is the first room in the stage which is fairly difficult"/>
<roomnamex="18"y="1"english="Wheeler's Wormhole"translation="الثقب الدودي، حسب ويلر"explanation="Apparently a scientist named John Wheeler coined the phrase 'Wormhole'! I just found that out. Anyway, this room has a teleporter in it."/>
<roomnamex="18"y="2"english="Sweeney's Maze"translation="متاهة سويني"explanation="Contains enemies that move strangly and resemble enemies from ZZT, an old game by Tim Sweeney"/>
<roomnamex="18"y="3"english="Mind The Gap"translation="حذار من الفجوة"explanation="Refers to what train announcers say on the London Underground when you leave the train - mind the gap between the train and the station platform"/>
<roomnamex="18"y="7"english="A Wrinkle in Time"translation="بين طيات الزمان"explanation="The name of a 60's science fiction book. There is a teleporter in this room."/>
<roomnamex="18"y="8"english="Getting Here is Half the Fun"translation="الوصول نصف المتعة"explanation="The top of the Veni Vidi Vici sequence. When you get to the top, you have to go all the way back down - hence, this room is the halfway point of the challenge."/>
<roomnamex="18"y="9"english="Your Bitter Tears... Delicious"translation="بكيت بمرارة... دموعك حلاوة"explanation="Part of the Veni Vidi Vici sequence. Taunting the player."/>
<roomnamex="18"y="10"english="Easy Mode Unlocked"translation="فتحت الطور السهل"explanation="Part of the Veni Vidi Vici sequence. Taunting the player - the 'easy mode' refers to the second passageway on the right side that is easier to take when going back through this room on the way down, but that passageway leads to death."/>
<roomnamex="18"y="11"english="Vici!"translation="جئت!"explanation="The rooms Veni, Vidi, Vici! appear in sequence. Famous latin phrase attributed to Julius Caesar meaning I came, I saw, I conquered. The hardest challenge in the game."/>
<roomnamex="18"y="12"english="Vidi"translation="نظرت!"explanation="The rooms Veni, Vidi, Vici! appear in sequence. Famous latin phrase attributed to Julius Caesar meaning I came, I saw, I conquered. The hardest challenge in the game."/>
<roomnamex="18"y="13"english="Veni"translation="فزت!"explanation="The rooms Veni, Vidi, Vici! appear in sequence. Famous latin phrase attributed to Julius Caesar meaning I came, I saw, I conquered. The hardest challenge in the game."/>
<roomnamex="18"y="14"english="Doing Things The Hard Way"translation="صعبت الطريق على نفسك"explanation="The starting room from the Veni, Vidi, Vici! challenge."/>
<roomnamex="19"y="0"english="To The Batcave!"translation="إلى مغارة الوطواط!"explanation="The Batcave, as in Batman's hideout."/>
<roomnamex="19"y="1"english="Ascending and Descending"translation="صواعد ونوازل"explanation="Just meant as in literally Ascending and Descending, going up and down. There is a room later in the game called Upstairs, Downstairs, which is a callback to this."/>
<roomnamex="19"y="2"english="Shockwave Rider"translation="راكب الموجات"explanation="Named after a 70's Science Fiction novel"/>
<roomnamex="19"y="3"english="This will make you flip"translation="سيقلب هذا مزاجك"explanation="Flip is used here in the sense 'Flip out', as in, to lose your temper"/>
<roomnamex="41"y="51"english="1950 Silverstone Grand V"translation="كأvv سلفرستون 1950"explanation="Refers to 1950 Silverstone Grand Prix (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="41"y="52"english="DIY V Repair"translation="تصليح V بدون معلم"explanation="DIY TV repair - this room has a television you can interact with, which changes the theme from black and white to colour"/>
<roomnamex="41"y="56"english="Now Take My Lead"translation="والآن ستمشي ورائي"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="42"y="52"english="Party Time!"translation="حفلة بلا ميعاد!"explanation="The first room after the black and white section. Doesn't refer to any TV show."/>
<roomnamex="42"y="56"english="What Are You Waiting For?"translation="ماذا تنتظر؟"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="43"y="51"english="The Voon Show"translation="برنامج القلاﺑvv"explanation="Named after The Goon Show (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="43"y="52"english="Upstairs, Downstairs"translation="طالع، نازل"explanation="Named after the 70s TV show - but also, refers to the earlier room Ascending and Descending, which this room is an updated version of. (Second part of the final stage has references to Colour TV shows)"/>
<roomnamex="43"y="56"english="Don't Get Ahead of Yourself!"translation="إياك واستباق الأمور!"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="44"y="51"english="Vertigo"translation="٧وار المرتفعات"explanation="Named after the Hitchcock film (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="44"y="52"english="Timeslip"translation="مخطوف الزمان"explanation="Named after the 70s TV show - name also suggests a connection to the earlier room Backsliders, which this room is an updated version of (Second part of the final stage has references to Colour TV shows)"/>
<roomnamex="44"y="56"english="Very Good"translation="حسن جدا"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="45"y="52"english="Three's Company"translation="سر الثلاثة في حفظ الأمناء"explanation="Named after the 70s sitcom - but also, suggests a connection to the earlier room Two's Company, which this room is an updated version of (Second part of the final stage has references to Colour TV shows)"/>
<roomnamex="45"y="56"english="Must I Do Everything For You?"translation="هل أساعدك في الصغيرة والكبيرة؟"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="46"y="54"english="Temporary Fault..."translation="عطل مؤقت..."explanation="Opening room of the final level."/>
<roomnamex="46"y="56"english="Now Stay Close To Me..."translation="فلنمش قريبين من بعض..."explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="47"y="52"english="Cosmic Creepers"translation="المتلصص الفضائي"explanation="Named after the cat from the 70s film Bedknobs and Broomsticks. Not sure why!"/>
<roomnamex="47"y="54"english="Do Not Adjust the V-hold"translation="لا حاجة لتحريك الهوائي ٧"explanation="I don't think V-hold is a real thing, it's supposed to just suggest 'Do not adjust your TV settings'. (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="47"y="56"english="...But Not Too Close"translation="...لكن بدون قرب زائد"explanation="In this room, if you go too quickly, your crewmate will walk into spikes."/>
<roomnamex="48"y="52"english="The Villi People"translation="معي غليظ"explanation="Bennett just thought this room looked 'intestinal'. Villi as in part of the intestinal system. Also refers to the 80s band The Village People."/>
<roomnamex="48"y="54"english="Regular Service Will Return Shortly"translation="سنعود بعد قليل"explanation="This is something you might hear on a TV station if they had lost reception. (The final stage has room names that suggest old black and white TV shows. A really, really good way to translate this level would be to use the names of black and white TV shows that are well known in your language, rather than trying to keep the exact meaning of the TV shows used here.)"/>
<roomnamex="48"y="56"english="Don't Be Afraid"translation="لا داع للخوف"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="49"y="52"english="Panic Room"translation="غرفة الذعر"explanation="A panic room is a safe room that you can hide in during an emergency, but I don't think that was Bennett's intention with this name. This room suddenly starts scrolling as soon as you enter, causing a panic."/>
<roomnamex="49"y="54"english="Origami Room"translation="غرفة في طي النسيان"explanation="As in folded paper - this room is mirrored around the center point."/>
<roomnamex="49"y="56"english="Do as I Say..."translation="طالما سمعت كلامي..."explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="50"y="51"english="The V Stooges"translation="البطل الخماسي"explanation="Refers to the 3 Stooges (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="50"y="52"english="1954 World Cup Vinyl"translation="فينيل كأvv العالم 1954"explanation="Refers to the World Cup Final (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnamex="50"y="56"english="...Not as I Do"translation="...بدل تقليد أفعالي"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="51"y="53"english="The Final Challenge"translation="التحدي النهائي"explanation="One of the last challenges in the game."/>
<roomnamex="51"y="56"english="Mind Your Head"translation="حذار، رأسك والسقف"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="52"y="53"english="The Last Straw"translation="القشة التي قصمت"explanation="One more little challenge, just after the room called The Final Challenge"/>
<roomnamex="52"y="56"english="Do Try To Keep Up"translation="هلا لحقتني، بدون تباطئ؟"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="53"y="48"english="Whee Sports"translation="`v´ ويهي ما أحلى الرياضة `v´"explanation="Refers to Wii Sports, the Nintendo Wii launch title. 'Whee' as in what a child might say while going down a slide (this room has a long drop in it)"/>
<roomnamex="53"y="49"english="Whizz Down The Shaft"translation="الصعوv إلى الهاوية"explanation="Whizz is Australian slang for doing something quickly, I think"/>
<roomnamex="53"y="50"english="The Gravitron"translation="أتون الجاذبية"explanation="A special arcade section where you have to survive for 60 seconds. Bennett named this one so as to suggest a funfair ride (though not any one in particular)."/>
<roomnamex="53"y="51"english="Tunnel of Terror"translation="أنفاق الأهوال"explanation="Another name inspired by funfairs."/>
<roomnamex="53"y="52"english="House of Mirrors"translation="متاهة المرايا"explanation="Another name inspired by funfairs."/>
<roomnamex="53"y="53"english="W"translation="W"explanation="This room has platforms in a W shape."/>
<roomnamex="53"y="56"english="You're Falling Behind"translation="تأخرت في النزول!"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnamex="54"y="48"english="VVVVVV"translation="VVVVVV"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 6/6"/>
<roomnamex="54"y="49"english="VVVVV"translation="VVVVV"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 5/6"/>
<roomnamex="54"y="50"english="VVVV"translation="VVVV"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 4/6"/>
<roomnamex="54"y="51"english="VVV"translation="VVV"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 3/6"/>
<roomnamex="54"y="52"english="VV"translation="VV"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 2/6"/>
<roomnamex="54"y="53"english="V"translation="V"explanation="Final sequence of rooms that spell out V-V-V-V-V-V, 1/6"/>
<roomnamex="54"y="56"english="Class Dismissed!"translation="انتهى الدرس!"explanation="This entire intermission section has a tone of a strict schoolteacher leading a small child."/>
<roomnameenglish="The Super Gravitron"translation="أتون الجاذبية الخارق"explanation="An expanded version of the room at 53, 50"/>
<roomnameenglish="I Can't Believe You Got This Far"translation="لا أصدق وصولك هذا الحد"explanation="If you're playing No Death Mode, the room Prize for the Reckless has this roomname instead (the room is altered to make it possible to do this section without dying)"/>
<roomnameenglish="Imagine Spikes There, if You Like"translation="يمكنك تخيل أشواك هناك لو شئت"explanation="If you're playing a time trial, the room Prize for the Reckless has this roomname instead (the room is altered to make it possible to do this section without dying)"/>
<!----->
<roomnameenglish="Rear Window"translation="النافذة الخلفية"explanation="Named after the Hitchcock film (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="On the Waterfront"translation="على جبهة البحر"explanation="Named after the 1954 film (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="On the Vaterfront"translation="على ٧بهة البحر"explanation=""/>
<!----->
<roomnameenglish="The Untouchables"translation="أناس فوق مستوى النقد"explanation="Before it was a film, the Untouchables was a TV series in 1959 (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="The Untouchavles"translation="أناس فوق مستوى ﺍﻟﻨﻘ٧"explanation=""/>
<!----->
<roomnameenglish="Television Newsveel"translation="نشرة الأنباء"explanation="Refers to Television Newsreel. (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="The 9 O'Clock News"translation="أخبار التاسعة"explanation="(Second part of the final stage has references to Colour TV shows)"/>
<!----->
<roomnameenglish="Vwitched"translation="المسحورة"explanation="Reference to early black and white sitcom Bewitched (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="Diav M for Mdrver"translation="الجريمة شبه الكاملة"explanation=""/>
<roomnameenglish="Dial M for Murder"translation="الجري[م]ة شبه الكاملة"explanation="Named after the Hitchcock film (Second part of the final stage has references to Colour TV shows)"/>
<!----->
<roomnameenglish="Gvnsmoke"translation="ويسترن"explanation="Gunsmoke was a black and white Western (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="Gunsmoke 1966"translation="غانسموك 1966"explanation="Gunsmoke changed to colour in 1966 (Second part of the final stage has references to Colour TV shows)"/>
<!----->
<roomnameenglish="Please enjoy these repeats"translation="استمتعوا بالإعادة"explanation="This stage also has a number of rooms which are harder versions of easier challenges, like this one. (The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="In the Margins"translation="على الهوامش"explanation="Not sure if this has a TV show reference, might just be meant literally (Second part of the final stage has references to Colour TV shows)"/>
<!----->
<roomnameenglish="Try Jiggling the Antenna"translation="فلنجرب خرخشة الهوائي"explanation="(The final stage has room names that suggest old black and white TV shows.)"/>
<roomnameenglish="Try Viggling the Antenna"translation="ڤلثچژپ څژڅشڤ الهڤائي"explanation=""/>
<roomnameenglish="Veavvn's Gvte"translation="أپڤاپ الچثڤ"explanation="Named after the 1980's film (Second part of the final stage has references to Colour TV shows)"/>
<roomnameenglish="Heaven's Gate"translation="أبواب الجنة"explanation="Named after the 1980's film (Second part of the final stage has references to Colour TV shows)"/>
<!-- Please read README.txt for information about the language files -->
<stringsmax_local_for="8x10">
<stringenglish="LOADING... {percent|digits=2|spaces}%"translation="جار التحميل...{percent|digits=2|spaces}%"explanation="on loading screen. {percent} = number from 0-100"max="26"max_local="26"/>
<stringenglish="continue from teleporter"translation="مواصلة من التنقيلة"explanation="menu option, game save that was made at a teleporter"/>
<stringenglish="Tele Save"translation="تخزينة تنقيلة"explanation="title, game save that was made at a teleporter"max="20"max_local="20"/>
<stringenglish="continue from quicksave"translation="مواصلة من التخزينة السريعة"explanation="menu option, game save that was made freely from the menu (at any checkpoint)"/>
<stringenglish="Quick Save"translation="تخزينة سريعة"explanation="title, game save that was made freely from the menu (at any checkpoint)"max="20"max_local="20"/>
<stringenglish="unlock play modes"translation="فتح أطوار اللعب"explanation="menu option"/>
<stringenglish="Unlock Play Modes"translation="فتح أطوار اللعب"explanation="title"max="20"max_local="20"/>
<stringenglish="Unlock parts of the game normally unlocked as you progress."translation="فتح أطوار اللعب فتحا عاديا حسب تقدمك في اللعبة."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="From here, you may unlock parts of the game that are normally unlocked as you play."translation="يمكنك من هنا فتح أجزاء من اللعبة فورا، كان من المفروض أن تفتح تدريجيا أثناء اللعب العادي."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="Change the language."translation="تغيير اللغة."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Can not change the language while a textbox is displayed in-game."translation="لا يمكن تغيير اللغة أثناء ظهور نص في اللعبة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="clear main game data"translation="حذف بيانات اللعبة الأساسية"explanation="menu option"/>
<stringenglish="clear custom level data"translation="حذف بيانات المراحل الاختيارية"explanation="menu option"/>
<stringenglish="Delete your main game save data and unlocked play modes."translation="حذف بيانات حفظ اللعبة الأساسية وما يخص أطوار اللعب المفتوحة."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="Delete your custom level save data and completion stars."translation="حذف بيانات المراحل الاختيارية وما يخص نجوم الإكمال."explanation="completion stars: when completing a level you get either 1 or 2 stars"max="38*5"max_local="38*5"/>
<stringenglish="Are you sure? This will delete your current saves..."translation="أحقا قررت ذلك؟
<stringenglish="Are you sure you want to delete all your saved data?"translation="هل تأكدت من قرارك بحذف كل بياناتك المحفوظة؟"explanation=""max="38*7"max_local="38*7"/>
<stringenglish="Are you sure you want to delete your quicksave?"translation="هل تأكدت من قرارك بحذف تخزينتك السريعة؟"explanation="only the quicksave of a custom level"max="38*7"max_local="38*7"/>
<stringenglish="no! don't delete"translation="لا! دعها ولا تحذفها"explanation="menu option"/>
<stringenglish="yes, delete everything"translation="أجل، احذف كل شيء"explanation="menu option"/>
<stringenglish="Choose letterbox/stretch/integer mode."translation="طريقة تكبير الصورة لتملأ الشاشة."explanation="See the `Current mode` explanations for more details on the modes"max="38*3"max_local="38*3"/>
<stringenglish="Current mode: INTEGER"translation="الوضع الحالي: مضاعف صحيح"explanation="integer (whole number) mode only enlarges the game in exact multiples of 320x240"max="38*2"max_local="38*2"/>
<stringenglish="Current mode: STRETCH"translation="الوضع الحالي: تمطيط الصورة"explanation="stretch mode just stretches the game to fill the window content"max="38*2"max_local="38*2"/>
<stringenglish="Current mode: LETTERBOX"translation="الوضع الحالي: حواف سوداء"explanation="letterbox mode enlarges the game to the window, but adds black bars to make the aspect ratio 4:3"max="38*2"max_local="38*2"/>
<stringenglish="resize to nearest"translation="التكبير لأقرب مضاعف"explanation="menu option. The game will be resized to the closest multiple of the normal resolution, 320x240, so for example if your window is 682x493, it will resize to 640x480"/>
<stringenglish="Resize to Nearest"translation="تكبير لأقرب مضاعف"explanation="title. The game will be resized to the closest multiple of the normal resolution, 320x240, so for example if your window is 682x493, it will resize to 640x480"max="20"max_local="20"/>
<stringenglish="Resize to the nearest window size that is of an integer multiple."translation="تكبير لأقرب حجم شاشة يكون عددا مضاعفا صحيحا للأبعاد الأصلية."explanation="The game will be resized to the closest multiple of the normal resolution, 320x240, so for example if your window is 682x493, it will resize to 640x480"max="38*3"max_local="38*3"/>
<stringenglish="You must be in windowed mode to use this option."translation="يشترط هذا الخيار وضع النافذة."explanation="The game cannot be in fullscreen to resize the window"max="38*2"max_local="38*2"/>
<stringenglish="Current mode: NEAREST"translation="الوضع الحالي: أقرب جار"explanation="nearest neighbor filter"max="38*2"max_local="38*2"/>
<stringenglish="toggle analogue"translation="تفعيل صورة البث التناظري"explanation="menu option, analogue mode simulates distortion from bad signal"/>
<stringenglish="Analogue Mode"translation="صورة البث التناظري"explanation="title, analogue mode simulates distortion from bad signal"max="20"max_local="20"/>
<stringenglish="There is nothing wrong with your television set. Do not attempt to adjust the picture."translation="تلفازك بخير. لا تحاول تصحيح صورته."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="toggle fps"translation="تفعيل حد الإطارات"explanation="menu option, kind of a misnomer, toggle between 30 FPS and more than 30 FPS"/>
<stringenglish="With Music by"translation="موسيقاها من"explanation="credits"max="30"max_local="30"/>
<stringenglish="Rooms Named by"translation="أسماء غرفها من"explanation="credits"max="30"max_local="30"/>
<stringenglish="C++ Port by"translation="نقلها إلى ++C من"explanation="credits"max="30"max_local="30"/>
<stringenglish="Patrons"translation="داعموها"explanation="credits, people who donated a certain amount before the game originally released"max="13"max_local="13"/>
<stringenglish="VVVVVV is supported by the following patrons"translation="لعبة VVVVVV يدعمها المتبرعون المذكورون"explanation="credits"max="38*3"max_local="38*3"/>
<stringenglish="and also by"translation="علاوة على"explanation="credits, VVVVVV is also supported by the following people"max="38*2"max_local="38*2"/>
<stringenglish="and"translation="ومعهم"explanation="credits. This list of names, AND furthermore, this other list of names"max="38"max_local="38"/>
<stringenglish="GitHub Contributors"translation="مساهمو غيتهب"explanation="credits. This doesn't _really_ need `GitHub` specifically, it could be replaced with `Code`"max="20"max_local="20"/>
<stringenglish="With contributions on GitHub from"translation="مع مساهمات برمجة غيتهب من"explanation="credits"max="40"max_local="40"/>
<stringenglish="and thanks also to:"translation="مع شكر خاص كذلك:"explanation="credits, and thanks also to ... you!"max="38*3"max_local="38*3"/>
<stringenglish="You!"translation="لك أنت!"explanation="credits, and thanks also to ... you!"max="20"max_local="20"/>
<stringenglish="Your support makes it possible for me to continue making the games I want to make, now and into the future."translation="دعمكم يمكنني من الاستمرار في صنع الألعاب التي أريدها اليوم ومستقبلا."explanation="credits"max="38*5"max_local="38*5"/>
<stringenglish="You cannot save in this mode."translation="لا يمكنك الحفظ في هذا الطور."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Would you like to disable the cutscenes during the game?"translation="هل تأكدت من قرارك بتعطيل المشاهد أثناء اللعبة؟"explanation=""max="38*6"max_local="38*6"/>
<stringenglish="Flip is bound to: "translation="العكس معين للزر: "explanation="controller binds, bound to A, B, X, Y, etc. These strings end with a space!"max="32"max_local="32"/>
<stringenglish="Enter is bound to: "translation="الدخول معين للزر: "explanation="controller binds, bound to A, B, X, Y, etc. These strings end with a space!"max="32"max_local="32"/>
<stringenglish="Menu is bound to: "translation="القائمة معينة للزر: "explanation="controller binds, bound to A, B, X, Y, etc. These strings end with a space!"max="32"max_local="32"/>
<stringenglish="Restart is bound to: "translation="إعادة الغرفة معينة للزر: "explanation="in-game death key to restart at checkpoint. Controller binds, bound to A, B, X, Y, etc. These strings end with a space!"max="32"max_local="32"/>
<stringenglish="Interact is bound to: "translation="التفاعل معين للزر: "explanation="controller binds, bound to A, B, X, Y, etc. These strings end with a space!"max="32"max_local="32"/>
<stringenglish="Press a button...|(or press ↑↓)"translation="يرجى ضغط زر...|(أو ضغط ↑↓)"explanation="the arrows represent up/down buttons, or stick movement... So: press a controller button, or navigate away"max="38*2"max_local="38*2"/>
<stringenglish="Add {button}?|Press again to confirm"translation="إضافة {button}؟|اضغط ثانية للتأكيد"explanation="Bind the X button to this action? Press X again to really add it"max="38*2"max_local="38*2"/>
<stringenglish="Remove {button}?|Press again to confirm"translation="إزالة {button}؟|اضغط ثانية للتأكيد"explanation="Remove the binding of the X button for this action? Press X again to really remove it"max="38*2"max_local="38*2"/>
<stringenglish="Interact is currently Enter!|See speedrunner options."translation="زر التفاعل حاليا Enter !|راجع إعدادات التختيم السريع."explanation="the Interact action can't be configured now because it's the same as the Enter action. There's an option in the Speedrunner options to split it off"max="38*2"max_local="38*2"/>
<stringenglish="ERROR: No language files found."translation="خطأ: لم نجد ملفات اللغة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Repository language folder:"translation="مجلد اللغة في غيتهب: "explanation="Language folder from the Git repository"max="39"max_local="39"/>
<stringenglish="Some options that are useful for translators and developers."translation="بعض الاختيارات التي قد تفيد المترجمين والمطورين"explanation=""max="38*6"max_local="38*6"/>
<stringenglish="maintenance"translation="صيانة"explanation="menu option, menu that allows you to apply maintenance to translations"/>
<stringenglish="open lang folder"translation="فتح مجلد اللغة"explanation="menu option. Button that opens the folder with language files (which is called lang) in a file explorer"/>
<stringenglish="Sync all language files after adding new strings."translation="مزامنة كل ملفات اللغة بعد إضافة سطور جديدة."explanation=""max="38*6"max_local="38*6"/>
<stringenglish="translate room names"translation="ترجمة أسماء الغرف"explanation="menu option"/>
<stringenglish="Enable room name translation mode, so you can translate room names in context. Press I for invincibility."translation="تفعيل وضع ترجمة أسماء الغرف، حتى تترجمها في سياقها. اضغط I للحصانة."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="You have not enabled room name translation mode!"translation="لم يفعل وضع ترجمة أسماء الغرف."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="Cycle through most menus in the game. The menus will not actually work, all options take you to the next menu instead. Press Escape to stop."translation="يتفحص معظم قوائم اللعبة. لا تعمل عملها الأصلي، بل تنقلك للقائمة المختبرة الأخرى. اضغط ESC للتوقف."explanation=""max="38*6"max_local="38*6"/>
<stringenglish="Display all text boxes from cutscenes.xml. Only tests the basic appearance of each individual text box."translation="إظهار كل صناديق النصوص من cutscenes.xml لكنه لا يختبر إلا المظهر العام للنص دون سواه."explanation=""max="38*6"max_local="38*6"/>
<stringenglish="from clipboard"translation="من ذاكرة النسخ"explanation="menu option, paste script name from clipboard"/>
<stringenglish="Explore the rooms of any level in the game, to find all room names to translate."translation="استطلاع غرف أي مستوى من اللعبة لكشف كل أسماء الغرف التي تحتاج ترجمات."explanation=""max="38*6"max_local="38*6"/>
<stringenglish="global limits check"translation="تحقق الحد الشامل"explanation="menu option"/>
<stringenglish="Limits check"translation="التحقق من الحدود"explanation="title"max="20"max_local="20"/>
<stringenglish="Find translations that don't fit within their defined bounds."translation="البحث عن ترجمات جاوزت حدودها."explanation=""max="38*6"max_local="38*6"/>
<stringenglish="No text overflows found!"translation="لم نجد فيضان نصوص!"explanation="limits check. no strings go outside their max bounds"max="38*6"max_local="38*6"/>
<stringenglish="No text overflows left!"translation="لم نبق على فيضان نصوص!"explanation="limits check. we have seen all strings that go outside their max bounds"max="38*6"max_local="38*6"/>
<stringenglish="Note that this detection isn't perfect."translation="مع الملاحظة أن هذا التحقق ليس متقنا."explanation="limits check"max="38*6"max_local="38*6"/>
<stringenglish="sync language files"translation="مزامنة ملفات اللغة"explanation="menu option"/>
<stringenglish="Sync language files"translation="مزامنة ملفات اللغة"explanation="title, translation maintenance menu"max="20"max_local="20"/>
<stringenglish="Merge all new strings from the template files into the translation files, keeping existing translations."translation="توحيد كل السطور الجديدة من ملفات التمبليت إلى ملفات الترجمة بالحفاظ على السطور الكاملة السابقة."explanation="translation maintenance menu"max="38*6"max_local="38*6"/>
<stringenglish="Count the amount of untranslated strings for this language."translation="عدد السطور التي لم تترجم لهذه اللغة"explanation=""max="38*6"max_local="38*6"/>
<stringenglish="Count the amount of untranslated strings for each language."translation="عدد السطور التي لم تترجم لكل لغة"explanation="translation maintenance menu"max="38*6"max_local="38*6"/>
<stringenglish="If new strings were added to the English template language files, this feature will insert them in the translation files for all languages. Make a backup, just in case."translation="لو أضيفت سطور إنكليزية، تضيفها هذه الميزة لنصوص اللغات الأخرى. احتياطا، احتفظ بنسخة سلفا."explanation="translation maintenance menu"max="38*7"max_local="38*7"/>
<stringenglish="Full syncing EN→All:"translation="مزامنة شاملة من الانكليزية للكل:"explanation="translation maintenance menu. The following list of language files can be fully synced from English to all other languages by pressing this button - new strings will be inserted in all languages"max="40"max_local="40"/>
<stringenglish="Syncing not supported:"translation="المزامنة غير مدعومة:"explanation="translation maintenance menu. The following list of language files are untouched by the sync button"max="40"max_local="40"/>
<stringenglish="All other gameplay settings."translation="كل إعدادات اللعبة الأخرى"explanation="description for advanced options"max="38*5"max_local="38*5"/>
<stringenglish="unfocus pause"translation="إزالة تركيز الراحة"explanation="menu option. Turns the pause screen on/off, which shows up when the window is unfocused/inactive (only one window is normally in focus/active at a time). Possible alternative: auto pause screen"/>
<stringenglish="Unfocus Pause"translation="إزالة تركيز الراحة"explanation="title. Turns the pause screen on/off, which shows up when the window is unfocused/inactive (only one window is normally in focus/active at a time). Possible alternative: auto pause screen"max="20"max_local="20"/>
<stringenglish="Toggle if the game will pause when the window is unfocused."translation="يحدد هل ستتوقف اللعبة لو زال التركيز عن النافذة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Unfocus pause is OFF"translation="تركيز الراحة معطل"explanation="Making another window active will not show the pause screen."max="38*2"max_local="38*2"/>
<stringenglish="Unfocus pause is ON"translation="تركيز الراحة مفعل"explanation="Making another window active will show the pause screen."max="38*2"max_local="38*2"/>
<stringenglish="unfocus audio pause"translation="الكتم عند زوال التركيز"explanation="menu option. Allows the user to choose whether the music should pause, or continue to play, when the window is unfocused/inactive (only one window is normally in focus/active at a time). Possible alternative: auto audio pause"/>
<stringenglish="Unfocus Audio"translation="الكتم عند زوال التركيز"explanation="title. Allows the user to choose whether the music should pause, or continue to play, when the window is unfocused/inactive (only one window is normally in focus/active at a time). Possible alternative: auto audio pause"max="20"max_local="20"/>
<stringenglish="Toggle if the audio will pause when the window is unfocused."translation="يحدد هل سيكتم صوت اللعبة لو زال التركيز عن النافذة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Unfocus audio pause is OFF"translation="الصمت عند زوال التركيز معطل"explanation="Making another window active will leave the music keep playing."max="38*2"max_local="38*2"/>
<stringenglish="Unfocus audio pause is ON"translation="الصمت عند زوال التركيز مفعل"explanation="Making another window active will pause the music."max="38*2"max_local="38*2"/>
<stringenglish="toggle in-game timer"translation="خيار إظهار المؤقت أثناء اللعبة"explanation="menu option"/>
<stringenglish="In-Game Timer"translation="المؤقت أثناء اللعبة"explanation="title"max="20"max_local="20"/>
<stringenglish="Toggle the in-game timer outside of time trials."translation="إظهار المؤقت في اللعبة حتى لغير التحدي ضد الساعة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="In-Game Timer is ON"translation="المؤقت أثناء اللعبة مفعل"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="In-Game Timer is OFF"translation="المؤقت أثناء اللعبة معطل"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Show the original English word enemies regardless of your language setting."translation="تظهر صور الأعداء بنسختها الإنكليزية الأصلية، بصرف النظر عن اللغة المختارة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Sprites are currently translated"translation="صور الأعداء مترجمة الآن"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Sprites are currently ALWAYS ENGLISH"translation="صور الأعداء دوما بالإنكليزية الآن"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Interact Button"translation="زر التفاعل"explanation="title, lets the user change the key for interacting with objects or crewmates"max="20"max_local="20"/>
<stringenglish="Toggle whether you interact with prompts using ENTER or E."translation="هل التفاعل مع النصوص بزر ENTER أو E."explanation="prompts: see the `Press {button} to talk to .../activate terminal/teleport` below"max="38*3"max_local="38*3"/>
<stringenglish="E"translation="E"explanation="keyboard key E. Speedrunner options menu"/>
<stringenglish="ACTION"translation="زر الفعل"explanation="the ACTION key is either the SPACE key, Z or V (this is explained on the title screen). It's used in strings like `Press ACTION to advance text`"/>
<stringenglish="Interact button: {button}"translation="زر التفاعل: {button}"explanation="keyboard key (E or ENTER) is filled in for {button}. Speedrunner options menu"max="38*2"max_local="38*2"/>
<stringenglish="Fake Load Screen"translation="شاشة التحميل المزيفة"explanation="title, allows the loading screen which counts to 100% to be turned off"max="20"max_local="20"/>
<stringenglish="Disable the fake loading screen which appears on game launch."translation="تعطيل شاشة التحميل المزيفة التي تظهر قبل اللعبة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Fake loading screen is OFF"translation="شاشة التحميل المزيفة معطلة"explanation="allows the loading screen which counts to 100% to be turned off"max="38*2"max_local="38*2"/>
<stringenglish="Fake loading screen is ON"translation="شاشة التحميل المزيفة تعمل"explanation="allows the loading screen which counts to 100% to be turned off"max="38*2"max_local="38*2"/>
<stringenglish="room name background"translation="خلفية اسم الغرفة"explanation="menu option, background behind room names"/>
<stringenglish="Room Name BG"translation="خلفية اسم الغرفة"explanation="title, background behind room names"max="20"max_local="20"/>
<stringenglish="Lets you see through what is behind the name at the bottom of the screen."translation="وضوح رؤية ما وراء اسم الغرفة أسفل الشاشة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Room name background is TRANSLUCENT"translation="خلفية اسم الغرفة شفافة"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Room name background is OPAQUE"translation="خلفية اسم الغرفة ليست شفافة"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="checkpoint saving"translation="تخزين نقطة الحفظ"explanation="menu option"/>
<stringenglish="Checkpoint Saving"translation="تخزين نقطة الحفظ"explanation="title, makes checkpoints save the game"max="20"max_local="20"/>
<stringenglish="Toggle if checkpoints should save the game."translation="تفعيل تخزين التقدم عند كل نقطة حفظ."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Checkpoint saving is OFF"translation="نقاط الحفظ لا تخزن التقدم"explanation="makes checkpoints save the game"max="38*2"max_local="38*2"/>
<stringenglish="Checkpoint saving is ON"translation="نقاط الحفظ تخزن التقدم"explanation="makes checkpoints save the game"max="38*2"max_local="38*2"/>
<stringenglish="Access some advanced settings that might be of interest to speedrunners."translation="دخول إعدادات متقدمة قد تحظى باهتمام
من يختمون الألعاب بسرعة."explanation="description for speedrunner options"max="38*5"max_local="38*5"/>
<stringenglish="glitchrunner mode"translation="إعدادات صائد العيوب"explanation="menu option, a glitchrunner is a speedrunner who takes advantage of glitches to run through the game faster"/>
<stringenglish="Glitchrunner Mode"translation="إعدادات صائد العيوب"explanation="title, a glitchrunner is a speedrunner who takes advantage of glitches to run through the game faster"max="20"max_local="20"/>
<stringenglish="Re-enable glitches that existed in previous versions of the game."translation="صائد العيوب يستغلها ليختم اللعبة بسرعة. هذا الوضع يعيد تفعيل عيوب برمجية من إصدارات سابقة سبق إصلاحها."explanation="glitchrunner mode"max="38*3"max_local="38*3"/>
<stringenglish="Glitchrunner mode is OFF"translation="وضع صائد العيوب السريع معطل."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Glitchrunner mode is {version}"translation="وضع صائد العيوب السريع مفعل."explanation="a version number is filled in for {version}, such as 2.0 or 2.2"max="38*2"max_local="38*2"/>
<stringenglish="Select a new glitchrunner version below."translation="يرجى اختيار إصدار جديد لصائد العيوب السريع."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="none"translation="بدون"explanation="menu option, do not emulate any older version"/>
<stringenglish="2.0"translation="2.0"explanation="VVVVVV version number for glitchrunner mode"/>
<stringenglish="2.2"translation="2.2"explanation="VVVVVV version number for glitchrunner mode"/>
<stringenglish="input delay"translation="تأجيل الضغطة"explanation="menu option, enable 1 frame of delay after pressing input"/>
<stringenglish="Input Delay"translation="تأجيل الضغطة"explanation="title, enable 1 frame of delay after pressing input"max="20"max_local="20"/>
<stringenglish="Re-enable the 1-frame input delay from previous versions of the game."translation="إرجاع تأجيل الضغطة بإطار واحد كما كان الحال في تحديثات اللعبة السابقة"explanation="input delay"max="38*3"max_local="38*3"/>
<stringenglish="Input delay is ON"translation="تأجيل الضغطة يعمل"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Input delay is OFF"translation="تأجيل الضغطة معطل"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Disable screen effects, enable slowdown modes or invincibility."translation="تعطيل بعض مؤثرات الشاشة، تفعيل أطوار لإبطاء اللعبة أو للحصانة من الأذى."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="Disable animated backgrounds in menus and during gameplay."translation="تعطيل الخلفيات المتحركة في القوائم وأثناء اللعب."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Backgrounds are ON."translation="الخلفيات تعمل."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Backgrounds are OFF."translation="الخلفيات معطلة."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Explore the game freely without dying. (Can cause glitches.)"translation="للتجول بحرية في اللعبة بدون أن تموت. قد تظهر عيوب برمجية."explanation="invincibility mode"max="38*3"max_local="38*3"/>
<stringenglish="Invincibility is ON."translation="الحصانة تعمل."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Invincibility is OFF."translation="الحصانة معطلة."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Are you sure you want to enable invincibility?"translation="ستفعل الحصانة من الأضرار.
<stringenglish="Who do you want to play the level with?"translation="مع من أردت لعب هذا المستوى؟"explanation="choose your NPC companion"max="38*8"max_local="38*8"/>
<stringenglish="time trials"translation="تحدي ضد الساعة"explanation="menu option"/>
<stringenglish="Time Trials"translation="تحدي ضد الساعة"explanation="title"max="20"max_local="20"/>
<stringenglish="Replay any level in the game in a competitive time trial mode."translation="يمكنك إعادة أي مستوى في اللعبة في طور تنافسي ضد الساعة."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="Time Trials are not available with slowdown or invincibility."translation="تحدي ضد الساعة لا يتوفر مع الحصانة أو التبطئة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="unlock time trials"translation="فتح تحدي ضد الساعة"explanation="menu option"/>
<stringenglish="Unlock Time Trials"translation="فتح تحدي ضد الساعة"explanation="title"max="20"max_local="20"/>
<stringenglish="You can unlock each time trial separately."translation="يمكنك فتح كل تحدي ضد الساعة على حدة."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="Replay the intermission levels."translation="إعادة لعب مراحل وصلات الفواصل"explanation=""max="38*3"max_local="38*3"/>
<stringenglish="unlock intermissions"translation="فتح وصلات الفواصل"explanation="menu option"/>
<stringenglish="TO UNLOCK: Complete the intermission levels in-game."translation="الشرط: ختم مستويات وصلات الفواصل أثناء اللعب."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="no death mode"translation="تحدي بدون الموت"explanation="menu option"/>
<stringenglish="No Death Mode"translation="تحدي بدون الموت"explanation="title"max="20"max_local="20"/>
<stringenglish="Play the entire game without dying once."translation="تختيم اللعبة بأكملها دون الموت ولا مرة."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="No Death Mode is not available with slowdown or invincibility."translation="تحدي بدون الموت لا يتوفر مع الحصانة أو التبطئة."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="unlock no death mode"translation="فتح تحدي بدون الموت"explanation="menu option"/>
<stringenglish="TO UNLOCK: Achieve an S-rank or above in at least 4 time trials."translation="الشرط: تحقيق الرتبة S أو ما فوق في 4 تحديات ضد الساعة على الأقل."explanation="ranks are B A S V, see below"max="38*3"max_local="38*3"/>
<stringenglish="flip mode"translation="الوضع المعكوس"explanation="menu option, mirrors the entire game vertically"/>
<stringenglish="Flip Mode"translation="الوضع المعكوس"explanation="title, mirrors the entire game vertically"max="20"max_local="20"/>
<stringenglish="Flip the entire game vertically."translation="يعكس اللعبة بأكملها عموديا."explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Flip the entire game vertically. Compatible with other game modes."translation="يعكس اللعبة بأكملها عموديا. يتوافق مع أطوار اللعبة الأخرى."explanation=""max="38*4"max_local="38*4"/>
<stringenglish="unlock flip mode"translation="فتح الوضع المعكوس"explanation="menu option"/>
<stringenglish="You managed to reach:"translation="نجحت في وصول الغرفة:"explanation="you managed to reach the following room"max="40"max_local="40"/>
<stringenglish="Keep trying! You'll get there!"translation="فلنواصل المحاولة! سندرك هدفنا!"explanation="player died before managing to save anybody"max="38*2"max_local="38*2"/>
<stringenglish="Nice one!"translation="جميل!"explanation="player died after saving one crewmate"max="38*2"max_local="38*2"/>
<stringenglish="Wow! Congratulations!"translation="واو! مبارك، تهانينا!"explanation="player died after saving two crewmates"max="38*2"max_local="38*2"/>
<stringenglish="Incredible!"translation="شيء مذهل!"explanation="player died after saving three crewmates"max="38*2"max_local="38*2"/>
<stringenglish="Unbelievable! Well done!"translation="لا أصدق! أحسنت صنيعا!"explanation="player died after saving four crewmates"max="38*2"max_local="38*2"/>
<stringenglish="Er, how did you do that?"translation="هع، أنى لك هذا؟"explanation="player died even though they were finished, lol"max="38*2"max_local="38*2"/>
<stringenglish="You rescued all the crewmates!"translation="أنقذت كل أفراد الطاقم!"explanation=""max="40"max_local="40"/>
<stringenglish="A new trophy has been awarded and placed in the secret lab to acknowledge your achievement!"translation="أهديت جائزة جديدة ووضعت في المخبر السري إشادة بإنجازك!"explanation=""max="38*4"max_local="38*4"/>
<stringenglish="[Trinkets found]"translation="[ التذكارات التي وجدتها ]"explanation="amount of shiny trinkets found"max="40"max_local="40"/>
<stringenglish="[Number of Deaths]"translation="[ الميتات التي تكبدتها ]"explanation=""max="40"max_local="40"/>
<stringenglish="[Time Taken]"translation="[ الوقت الذي استغرقته ]"explanation="stopwatch time"max="40"max_local="40"/>
<stringenglish="Trinkets Found:"translation="التذكارات التي وجدتها:"explanation="game complete screen"max="22"max_local="22"/>
<stringenglish="{n_trinkets} of {max_trinkets}"translation="{n_trinkets} من أصل {max_trinkets}"explanation="ex: 2 of 5"/>
<stringenglish="{n_trinkets|wordy} out of {max_trinkets|wordy}"translation="{n_trinkets|wordy} من أصل {max_trinkets|wordy}"explanation="ex: One out of Twenty, see numbers.xml. You can add |upper for an uppercase letter."max="34"max_local="34"/>
<stringenglish="{savebox_n_trinkets|wordy}"translation="{savebox_n_trinkets|wordy}"explanation="trinket count in telesave/quicksave information box. You can add |upper for an uppercase letter."/>
<stringenglish="{gamecomplete_n_trinkets|wordy}"translation="{gamecomplete_n_trinkets|wordy}"explanation="trinket count on Game Complete screen (after Trinkets Found:) You can add |upper for an uppercase letter."/>
<stringenglish="+1 Rank!"translation="+1 للرتبة!"explanation="time trial rank was upgraded (B → A → S → V). B is minimum, which is purposefully high (see it as 7/10) - S is a popular rank above A in a lot of games, so VVVVVV added a rank above that."max="12"max_local="12"/>
<stringenglish="Not yet attempted"translation="لم تجرب من قبل"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="TO UNLOCK:"translation="شرط الفتح:"explanation="followed by `Rescue XX`/`Complete the game`, and then `Find XX trinkets`"max="40"max_local="40"/>
<stringenglish="Complete the game"translation="ختم اللعبة"case="0"explanation=""max="40"max_local="40"/>
<stringenglish="Find three trinkets"translation="إيجاد ثلاث تذكارات"explanation=""max="40"max_local="40"/>
<stringenglish="Find six trinkets"translation="إيجاد ست تذكارات"explanation=""max="40"max_local="40"/>
<stringenglish="Find nine trinkets"translation="إيجاد تسع تذكارات"explanation=""max="40"max_local="40"/>
<stringenglish="Find twelve trinkets"translation="إيجاد إثنتا عشر تذكار"explanation=""max="40"max_local="40"/>
<stringenglish="Find fifteen trinkets"translation="إيجاد خمسة عشر تذكار"explanation=""max="40"max_local="40"/>
<stringenglish="Find eighteen trinkets"translation="إيجاد ثمانية عشر تذكار"explanation=""max="40"max_local="40"/>
<stringenglish="RECORDS"translation="السجلات"explanation="followed by a list of personal bests for TIME, SHINY and LIVES. So `records` as in `world records`, except for yourself"max="15"max_local="15"/>
<stringenglish="Your save files have been updated."translation="حدثت بيانات تخزيناتك."explanation="player completed game"max="38*6"max_local="38*6"/>
<stringenglish="If you want to keep exploring the game, select CONTINUE from the play menu."translation="لو شئت مواصلة استكشاف اللعبة، فعليك بخيار المواصلة من قائمة اللعب."explanation=""max="38*9"max_local="38*9"/>
<stringenglish="You have unlocked a new Time Trial."translation="فتحت تحديا من جملة التحديات ضد الساعة."explanation=""max="38*7"max_local="38*7"/>
<stringenglish="You have unlocked some new Time Trials."translation="فتحت تحديا جديدا من جملة التحديات ضد الساعة."explanation=""max="38*7"max_local="38*7"/>
<stringenglish="You have unlocked No Death Mode."translation="فتحت تحدي بدون الموت."explanation=""max="38*7"max_local="38*7"/>
<stringenglish="You have unlocked Flip Mode."translation="فتحت الوضع المعكوس."explanation=""max="38*7"max_local="38*7"/>
<stringenglish="You have unlocked the intermission levels."translation="فتحت مستويات وصلات الفواصل."explanation=""max="38*7"max_local="38*7"/>
<stringenglish="play a level"translation="لعب مرحلة"explanation="menu option"/>
<stringenglish="open level folder"translation="فتح مجلد المراحل"explanation="menu option. Button that opens the folder with level files in a file explorer"/>
<stringenglish="back to levels"translation="العودة للمستويات"explanation="menu option"/>
<stringenglish="The level editor is not currently supported on Steam Deck, as it requires a keyboard and mouse to use."translation="محرر المستويات يحتاج لوح مفاتيح وفأرة. لهذا السبب، ليس مدعوما على منصة Steam Deck."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="The level editor is not currently supported on this device, as it requires a keyboard and mouse to use."translation="محرر المستويات يحتاج لوح مفاتيح وفأرة. لهذا السبب، ليس مدعوما على جهازك."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="To install new player levels, copy the .vvvvvv files to the levels folder."translation="لتنصيب مراحل اللاعبين الجديدة، تنسخ ملفات .vvvvvv إلى مجلد levels."explanation=""max="38*5"max_local="38*5"/>
<stringenglish="Are you sure you want to show the levels path? This may reveal sensitive information if you are streaming."translation="قبل إظهار مسار المستويات، يرجى التأكد لتجنب افتضاح أي معلومات حساسة أثناء البثوث. هل نظهره؟"explanation=""max="38*4"max_local="38*4"/>
<stringenglish="ACTION = Space, Z, or V"translation="زر الفعل = المسافة أو Z أو V"explanation="title screen"max="38*3"max_local="38*3"/>
<stringenglish="[Press {button} to return to editor]"translation="[نضغط {button} للعودة للمحرر]"explanation="`to editor` is sorta redundant"max="40"max_local="40"/>
<stringenglish="- Press {button} to advance text -"translation="- نضغط {button} للتقدم في النص -"explanation="to dismiss a textbox. Expect `ACTION`"max="40"max_local="40"/>
<stringenglish="Press {button} to continue"translation="نضغط {button} للمواصلة"explanation="Expect `ACTION`"max="34"max_local="34"/>
<stringenglish="[Press {button} to unfreeze gameplay]"translation="[نضغط {button} لوقف تجميد اللعب العادي]"explanation="in level debugger: {button} makes everything start moving as normal. Limit is treacherous, expect TAB for {button}. Frozen is the initial state, so this is the first string of the two that users will see!"max="39"max_local="39"/>
<stringenglish="[Press {button} to freeze gameplay]"translation="[نضغط {button} لتجميد اللعب العادي]"explanation="in level debugger: {button} makes everything stop moving. Limit is treacherous, expect TAB for {button}."max="39"max_local="39"/>
<stringenglish="NO SIGNAL"translation="لا إشارة"explanation="map screen. So like a TV/computer monitor"max="29"max_local="29"/>
<stringenglish="Press {button} to warp to the ship."translation="للتنقيل إلى السفينة، نضغط {button}."explanation="spaceship. Warp = teleport. Expect `ACTION`"max="38*7"max_local="38*7"/>
<stringenglish="Missing..."translation="مفقود..."case="1"explanation="this male crew member is missing"max="15"max_local="15"/>
<stringenglish="Missing..."translation="مفقودة..."case="2"explanation="this female crew member is missing"max="15"max_local="15"/>
<stringenglish="Missing..."translation="أين أنت..."case="3"explanation="Viridian is missing (final level). You could even fill in something like `Uh-oh...` here if you really have to specify gender otherwise - everyone else is rescued, but the player is missing"max="15"max_local="15"/>
<stringenglish="Rescued!"translation="أنقذته!"case="1"explanation="this male crew member is not missing anymore"max="15"max_local="15"/>
<stringenglish="Rescued!"translation="أنقذتها!"case="2"explanation="this female crew member is not missing anymore"max="15"max_local="15"/>
<stringenglish="(that's you!)"translation="(ها أنت!)"explanation="this crew member is you (Viridian)"max="15"max_local="15"/>
<stringenglish="Cannot Save in Level Replay"translation="لا يحق الحفظ عند مشاهدة المستوى"explanation="in-game menu"max="38*7"max_local="38*7"/>
<stringenglish="Cannot Save in No Death Mode"translation="لا يحق الحفظ في تحدي بدون الموت"explanation="in-game menu"max="38*7"max_local="38*7"/>
<stringenglish="How'd you get here?"translation="كيف وصلت هنا؟"explanation="in-game menu"max="38*7"max_local="38*7"/>
<stringenglish="Cannot Save in Secret Lab"translation="لا يحق الحفظ في المخبر السري"explanation="in-game menu"max="38*7"max_local="38*7"/>
<stringenglish="ERROR: Could not save game!"translation="خطأ: فشل حفظ تقدم اللعبة!"explanation="in-game menu"max="34*2"max_local="34*2"/>
<stringenglish="ERROR: Could not save settings file!"translation="خطأ: فشل حفظ ملف الإعدادات!"explanation=""max="38*2"max_local="38*2"/>
<stringenglish="Game saved ok!"translation="حفظت اللعبة على خير!"explanation="in-game menu"max="38*2"max_local="38*2"/>
<stringenglish="[Press {button} to save your game]"translation="[نضغط {button} لحفظ لعبتك]"explanation="in-game menu. Expect `ACTION`"max="40"max_local="40"/>
<stringenglish="(Note: The game is autosaved at every teleporter.)"translation="(اللعبة تحفظ تلقائيا عند كل آلة تنقيل.)"explanation="in-game menu"max="38*3"max_local="38*3"/>
<stringenglish="Return to main menu?"translation="هل بودك العودة للقائمة الرئيسية؟"explanation="in-game menu"max="38*4"max_local="38*4"/>
<stringenglish="Do you want to quit? You will lose any unsaved progress."translation="هل قررت المغادرة؟ قد يضيع
أي تقدم لم يحفظ بعد."explanation="in-game menu"max="38*4"max_local="38*4"/>
<stringenglish="Do you want to return to the secret laboratory?"translation="هل بودك العودة للمخبر السري؟"explanation="in-game menu"max="38*4"max_local="38*4"/>
<stringenglish="no, keep playing"translation="لا، سأواصل اللعب"explanation="in-game menu option"max="28"max_local="28"/>
<stringenglish="[ NO, KEEP PLAYING ]"translation="[ لا، سأواصل اللعب ]"explanation="in-game menu option"max="32"max_local="32"/>
<stringenglish="yes, quit to menu"translation="نعم، سأغادر"explanation="in-game menu option"max="24"max_local="24"/>
<stringenglish="[ YES, QUIT TO MENU ]"translation="[ نعم، سأغادر ]"explanation="in-game menu option"max="28"max_local="28"/>
<stringenglish="yes, return"translation="نعم، سأعود"explanation="in-game menu option"max="24"max_local="24"/>
<stringenglish="[ YES, RETURN ]"translation="[ نعم، سأعود ]"explanation="in-game menu option"max="28"max_local="28"/>
<stringenglish="no, return"translation="لا، سأعود"explanation="quit program menu option"/>
<stringenglish="yes, quit"translation="نعم، سأخرج"explanation="quit program menu option"/>
<stringenglish="return to game"translation="العودة للعبة"explanation="pause menu option"max="27"max_local="27"/>
<stringenglish="quit to menu"translation="الخروج للقائمة"explanation="pause menu option"max="19"max_local="19"/>
<stringenglish="Press Left/Right to choose a Teleporter"translation="نضغط يسار/يمين لاختيار آلة تنقيل"explanation="tight fit, so maybe `Left/Right: choose teleporter`, or use ←/→"max="40"max_local="40"/>
<stringenglish="Press {button} to Teleport"translation="نضغط {button} للتنقل"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="40"max_local="40"/>
<stringenglish="- Press {button} to Teleport -"translation="- نضغط {button} للتنقل -"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}"max="40"max_local="40"/>
<stringenglish="Press {button} to explode"translation="نضغط {button} للتفجر"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed. This was for testing, but people sometimes find it in custom levels"max="37"max_local="37"/>
<stringenglish="Press {button} to talk to Violet"translation="نضغط {button} للحديث مع فايولت"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to talk to Vitellary"translation="نضغط {button} للحديث مع فيتيلاري"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to talk to Vermilion"translation="نضغط {button} للحديث مع فارميليون"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to talk to Verdigris"translation="نضغط {button} للحديث مع فارديغري"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to talk to Victoria"translation="نضغط {button} للحديث مع فيكتوريا"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to activate terminal"translation="نضغط {button} لتفعيل الحاسب"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="Press {button} to activate terminals"translation="نضغط {button} لتفعيل الحواسيب"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed. there are 3 next to each other in the ship"max="37"max_local="37"/>
<stringenglish="Press {button} to interact"translation="نضغط {button} للتفاعل"explanation="keyboard key (E or ENTER) or controller button glyph is filled in for {button}. max is when displayed"max="37"max_local="37"/>
<stringenglish="- Press {button} to skip -"translation="- نضغط {button} للتجاوز -"explanation="keyboard key (ENTER) or controller button glyph is filled in for {button}. max is when displayed. This prompt is for skipping cutscenes"max="40"max_local="40"/>
<stringenglish="Passion for Exploring"translation="Passion for Exploring"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Pushing Onwards"translation="Pushing Onwards"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Positive Force"translation="Positive Force"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Presenting VVVVVV"translation="Presenting VVVVVV"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Potential for Anything"translation="Potential for Anything"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Predestined Fate"translation="Predestined Fate"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Pipe Dream"translation="Pipe Dream"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Popular Potpourri"translation="Popular Potpourri"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Pressure Cooker"translation="Pressure Cooker"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="ecroF evitisoP"translation="ecroF evitisoP"explanation="jukebox prompt. song name should probably not be translated"max="37*2"max_local="37*2"/>
<stringenglish="Map Settings"translation="إعدادات الغرفة"explanation="title, editor, Level Settings, map is kinda inconsistent with everything else"max="20"max_local="20"/>
<stringenglish="edit scripts"translation="تحرير السكربتات"explanation="level editor menu option"/>
<stringenglish="change music"translation="تغيير الموسيقى"explanation="level editor menu option"/>
<stringenglish="editor ghosts"translation="أشباح المحرر"explanation="level editor menu option. Toggles option to show a repetition of the player's path after playtesting"/>
<stringenglish="Editor ghost trail is OFF"translation="مسار شبح المحرر معطل"explanation="level editor. Repetition of the player's path after playtesting is disabled"max="40"max_local="40"/>
<stringenglish="Editor ghost trail is ON"translation="مسار شبح المحرر مفعل"explanation="level editor. Repetition of the player's path after playtesting is enabled"max="40"max_local="40"/>
<stringenglish="load level"translation="فتح المستوى"explanation="level editor menu option"/>
<stringenglish="save level"translation="حفظ المستوى"explanation="level editor menu option"/>
<stringenglish="quit to main menu"translation="الخروج للقائمة الرئيسية"explanation="level editor menu option"max="22"max_local="22"/>
<stringenglish="change name"translation="تبديل الاسم"explanation="level editor menu option"/>
<stringenglish="change author"translation="تبديل المؤلف"explanation="level editor menu option"/>
<stringenglish="change description"translation="تبديل الوصف"explanation="level editor menu option"/>
<stringenglish="change website"translation="تبديل موقع النت"explanation="level editor menu option"/>
<stringenglish="change font"translation="تبديل الخطوط"explanation="level editor menu option"/>
<stringenglish="Level Font"translation="خط نص المستوى"explanation="title, editor, font that a custom level will show up in. You can select a language name here (like Chinese or Japanese which have different fonts, or `other`)"max="20"max_local="20"/>
<stringenglish="Select the language in which the text in this level is written."translation="اختيار اللغة التي كتب بها نص هذا المستوى."explanation=""max="38*3"max_local="38*3"/>
<stringenglish="Font: "translation="الخط: "explanation="level options, followed by name of font (mind the space)"max="15"max_local="15"/>
<stringenglish="Map Music"translation="موسيقى الغرفة"explanation="title, editor, music that starts playing when a level is started. Can be changed with scripting later, so this is really just initial music"max="20"max_local="20"/>
<stringenglish="Current map music:"translation="موسيقى الغرفة الحالية:"explanation="editor, followed by the number and name of a song, or `No background music`. Can be changed with scripting later, so this is really just initial music"max="38*2"max_local="38*2"/>
<stringenglish="No background music"translation="لا موسيقى"explanation="editor, level starts with no song playing"max="38*2"max_local="38*2"/>
<stringenglish="1: Pushing Onwards"translation="1: Pushing Onwards"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="2: Positive Force"translation="2: Positive Force"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="3: Potential for Anything"translation="3: Potential for Anything"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="4: Passion for Exploring"translation="4: Passion for Exploring"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="N/A: Pause"translation="N/A: Pause"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="5: Presenting VVVVVV"translation="5: Presenting VVVVVV"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="N/A: Plenary"translation="N/A: Plenary"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="6: Predestined Fate"translation="6: Predestined Fate"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="N/A: ecroF evitisoP"translation="N/A: ecroF evitisoP"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="7: Popular Potpourri"translation="7: Popular Potpourri"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="8: Pipe Dream"translation="8: Pipe Dream"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="9: Pressure Cooker"translation="9: Pressure Cooker"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="10: Paced Energy"translation="10: Paced Energy"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="11: Piercing the Sky"translation="11: Piercing the Sky"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="N/A: Predestined Fate Remix"translation="N/A: Predestined Fate Remix"explanation="editor, song name should probably not be translated"max="38*2"max_local="38*2"/>
<stringenglish="?: something else"translation="?: شيء آخر"explanation="editor, song was not recognized"max="38*2"max_local="38*2"/>
<stringenglish="next song"translation="اللحن التالي"explanation="level editor menu option"/>
<stringenglish="previous song"translation="اللحن السابق"explanation="level editor menu option"/>
<stringenglish="back"translation="عودة"explanation="level editor menu option"/>
<stringenglish="Save before quitting?"translation="هل تود الحفظ قبل الخروج؟"explanation="level editor"max="38*4"max_local="38*4"/>
<stringenglish="yes, save and quit"translation="أجل، حفظ ثم خروج"explanation="level editor menu option"/>
<stringenglish="no, quit without saving"translation="لا، خروج بدون حفظ"explanation="level editor menu option"/>
<stringenglish="return to editor"translation="عودة للمحرر"explanation="level editor menu option"/>
<stringenglish="Untitled Level"translation="مستوى بلا عنوان"explanation=""max="20"max_local="20"/>
<stringenglish="SCRIPT BOX: Click on the first corner"translation="هامش السكربت: انقر الزاوية الأولى للصندوق"explanation="editor, a script box is an invisible box that runs a script when touched"max="39*3"max_local="39*3"/>
<stringenglish="SCRIPT BOX: Click on the last corner"translation="هامش السكربت: انقر الزاوية المقابلة للصندوق"explanation="editor, a script box is an invisible box that runs a script when touched"max="39*3"max_local="39*3"/>
<stringenglish="ENEMY BOUNDS: Click on the first corner"translation="هامش الأعداء: انقر الزاوية الأولى للصندوق"explanation="editor, invisible box which enemies always stay inside of"max="39*3"max_local="39*3"/>
<stringenglish="ENEMY BOUNDS: Click on the last corner"translation="هامش الأعداء: انقر الزاوية المقابلة للصندوق"explanation="editor, invisible box which enemies always stay inside of"max="39*3"max_local="39*3"/>
<stringenglish="PLATFORM BOUNDS: Click on the first corner"translation="هامش المنصات: انقر الزاوية الأولى للصندوق"explanation="editor, invisible box which moving platforms always stay inside of"max="39*3"max_local="39*3"/>
<stringenglish="PLATFORM BOUNDS: Click on the last corner"translation="هامش المنصات: انقر الزاوية المقابلة للصندوق"explanation="editor, invisible box which moving platforms always stay inside of"max="39*3"max_local="39*3"/>
<stringenglish="Click on the first corner"translation="انقر الزاوية الأولى"explanation=""max="39*3"max_local="39*3"/>
<stringenglish="Click on the last corner"translation="انقر الزاوية الأخيرة"explanation=""max="39*3"max_local="39*3"/>
<stringenglish="**** VVVVVV SCRIPT EDITOR ****"translation="**** محرر سكربتات VVVVVV ****"explanation="supposed to look like a Commodore 64 screen"max="36"max_local="36"/>
<stringenglish="PRESS ESC TO RETURN TO MENU"translation="اضغط ESC للعودة للقائمة"explanation="Commodore 64-style script editor, TO MENU is redundant"max="36"max_local="36"/>
<stringenglish="NO SCRIPT IDS FOUND"translation="لم نجد معرف ID للسكربتات"explanation="Commodore 64-style script editor, basically NO SCRIPTS FOUND"max="36"max_local="36"/>
<stringenglish="CREATE A SCRIPT WITH EITHER THE TERMINAL OR SCRIPT BOX TOOLS"translation="أنشئ سكربتا إما بالواجهة النصية أو بأدوات السكربتات"explanation="Commodore 64-style script editor"max="36*5"max_local="36*5"/>
<stringenglish="CURRENT SCRIPT: {name}"translation="السكربت الحالي: {name}"explanation="Commodore 64-style script editor. Char limit is soft, but the longer this is, the more often users" script names run offscreen. Consider SCRIPT: instead"max="20"max_local="20"/>
<stringenglish="Left click to place warp destination"translation="نقرة يسرى بالفأرة لوضع وجهة التنقيل"explanation="warp token: small teleporter with entrance and destination"max="39"max_local="39"/>
<stringenglish="Right click to cancel"translation="نقرة يمنى بالفأرة للتراجع"explanation=""max="39"max_local="39"/>
<stringenglish="{button1} and {button2} keys change tool"translation="الأزرار {button1} و {button2} لتغيير الأداة"explanation="These keys can be used to switch between tools"max="36"max_local="36"/>
<stringenglish="5: Checkpoints"translation="5: نقاط حفظ"explanation="editor tool. You restart at the last checkpoint after death"max="32"max_local="32"/>
<stringenglish="6: Disappearing Platforms"translation="6: منصات تتلاشى"explanation="editor tool. Platform that disappears when you step on it (disappearing platform, crumbling platform)"max="32"max_local="32"/>
<stringenglish="0: Gravity Lines"translation="0: خطوط جاذبية"explanation="editor tool. Gravity line, which flips your gravity when touching it"max="32"max_local="32"/>
<stringenglish="T: Terminals"translation="T: حواسيب"explanation="editor tool. Computer which can be activated by the player to run a script"max="32"max_local="32"/>
<stringenglish="Y: Script Boxes"translation="Y: هامش سكربت"explanation="editor tool. Invisible box that runs a script when touched"max="32"max_local="32"/>
<stringenglish="U: Warp Tokens"translation="U: نائب تنقيل"explanation="editor tool. Small teleporter with entrance and destination"max="32"max_local="32"/>
<stringenglish="I: Warp Lines"translation="I: خط تنقيل"explanation="editor tool. Makes a room edge be connected to its opposite edge"max="32"max_local="32"/>
<stringenglish="O: Crewmates"translation="O: عضو طاقم"explanation="editor tool. Crewmate that can be rescued"max="32"max_local="32"/>
<stringenglish="P: Start Point"translation="P: نقطة بدء"explanation="editor tool"max="32"max_local="32"/>
<stringenglish="START"translation="بدء"explanation="start point in level editor"max="10"max_local="10"/>
<stringenglish="SPACE ^ SHIFT ^"translation="SPACE ^ SHIFT ^"explanation="editor, indicates both SPACE key and SHIFT key open up menus. ^ is rendered as up arrow"max="32"max_local="32"/>
<stringenglish="F1: Change Tileset"translation="F1: تبديل مجموعة الخلايا"explanation="editor shortcut, switch to different tileset"max="25"max_local="25"/>
<stringenglish="F2: Change Colour"translation="F2: تبديل الألوان"explanation="editor shortcut, switch to different tileset color/variant"max="25"max_local="25"/>
<stringenglish="F10: Direct Mode"translation="F10: وضع الرسم المباشر"explanation="editor shortcut, direct mode is manual tile placing mode, where walls are not automatically made nice"max="25"max_local="25"/>
<stringenglish="W: Change Warp Dir"translation="W: تبديل اتجاه التنقيل"explanation="editor shortcut, dir=direction, change which edges of the room wrap around. Can be horizontal, vertical or all sides"max="25"max_local="25"/>
<stringenglish="E: Change Roomname"translation="E: تبديل اسم الغرفة"explanation="editor shortcut, the name of the room appears at the bottom"max="25"max_local="25"/>
<stringenglish="S: Save Map"translation="S: حفظ الغرفة"explanation="level editor, save level, `Map` is basically redundant"max="12"max_local="12"/>
<stringenglish="L: Load Map"translation="L: فتح الغرفة"explanation="level editor, load level, `Map` is basically redundant"max="12"max_local="12"/>
<stringenglish="Enter map filename to save as:"translation="اكتب مسار ملف الغرفة لحفظه:"explanation="level editor text input, save level file as"max="39*3"max_local="39*3"/>
<stringenglish="Enter map filename to load:"translation="اكتب مسار ملف الغرفة لفتحه:"explanation="level editor text input, load level file"max="39*3"max_local="39*3"/>
<stringenglish="Enter new room name:"translation="اكتب اسم الغرفة الجديدة:"explanation="level editor text input, the name of the room appears at the bottom"max="39*3"max_local="39*3"/>
<stringenglish="Enter room coordinates x,y:"translation="اكتب احداثيات الغرفة س,ص:"explanation="level editor text input, go to another room by its coordinates, for example, 9,15"max="39*3"max_local="39*3"/>
<stringenglish="Enter script name:"translation="اكتب اسم السكربت:"explanation="level editor text input, enter name of newly created script, or edit the script name of a terminal/script box"max="39*3"max_local="39*3"/>
<stringenglish="Enter roomtext:"translation="اكتب النص:"explanation="level editor text input, enter text to place in the room"max="39*3"max_local="39*3"/>
<stringenglish="Space Station"translation="المحطة الفضائية "explanation="editor tileset name, Now using Space Station Tileset"/>
<stringenglish="Outside"translation="الفضاء الخارجي"explanation="editor tileset name, Now using Outside Tileset"/>
<stringenglish="Lab"translation="المخبر"explanation="editor tileset name, Now using Lab Tileset"/>
<stringenglish="Warp Zone"translation="منطقة التنقيل"explanation="editor tileset name, Now using Warp Zone Tileset. The Warp Zone got its name because its rooms wrap around, but think Star Trek"/>
<stringenglish="Ship"translation="السفينة"explanation="editor tileset name, Now using Ship Tileset. Spaceship"/>
<stringenglish="Now using {area} Tileset"translation="سنغير لمجموعة الخلايا
من {area}"explanation="level editor, user changed the tileset of the room to {area} (like Ship, Lab, etc)"max="38*3"max_local="38*3"/>
<stringenglish="Tileset Colour Changed"translation="تغير لون مجموعة الخلايا"explanation="level editor, user changed the tileset colour/variant of the room"max="38*3"max_local="38*3"/>
<stringenglish="Enemy Type Changed"translation="تغير نوع الأعداء"explanation="level editor, user changed enemy appearance for the room"max="38*3"max_local="38*3"/>
<stringenglish="Platform speed is now {speed}"translation="تغيرت سرعة المنصات إلى {speed}"explanation="level editor, user changed speed of platforms for the room"max="38*3"max_local="38*3"/>
<stringenglish="Enemy speed is now {speed}"translation="تغيرت سرعة الأعداء إلى {speed}"explanation="level editor, user changed speed of enemies for the room"max="38*3"max_local="38*3"/>
<stringenglish="Reloaded resources"translation="أعيد فتح ملفات الموارد"explanation="level editor, reloaded graphics assets/resources, music and sound effects"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Invalid format"translation="خطأ: صيغة المكتوب غير مناسبة"explanation="user was supposed to enter something like `12,12`, but entered `as@df`"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Could not load level"translation="خطأ: فشل فتح الغرفة"explanation="that level could not be loaded, maybe it does not exist"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Could not save level!"translation="خطأ: فشل حفظ الغرفة!!"explanation="maybe the filename is too long? exclamation mark because you will lose your data if you quit"max="38*3"max_local="38*3"/>
<stringenglish="Mapsize is now [{width},{height}]"translation="أبعاد الغرفة الآن [{width},{height}]"explanation="editor, the map is now {width} by {height}"max="38*3"max_local="38*3"/>
<stringenglish="Direct Mode Disabled"translation="وضع الرسم المباشر معطل"explanation="editor, manual tile placing mode where walls are not automatically made nice, now changed to automatic mode"max="38*3"max_local="38*3"/>
<stringenglish="Direct Mode Enabled"translation="وضع الرسم المباشر مفعل"explanation="editor, manual tile placing mode where walls are not automatically made nice, now changed to manual mode"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Warp lines must be on edges"translation="خطأ: خطوط التنقيل ترسم على حواف الغرفة"explanation="level editor, warp lines make a room edge be connected to its opposite edge. So they must be placed on one of the edges, not in the middle"max="38*3"max_local="38*3"/>
<stringenglish="Room warps in all directions"translation="الغرفة تلتف على نفسها من كل الاتجاهات"explanation="level editor, all edges of the room are connected with each other. If the player leaves the room they end up on the other side of the same room."max="38*3"max_local="38*3"/>
<stringenglish="Room warps horizontally"translation="الغرفة تلتف على نفسها أفقيا"explanation="level editor, the left and right edges of the room are connected with each other. The player can still go to other rooms on the top and bottom"max="38*3"max_local="38*3"/>
<stringenglish="Room warps vertically"translation="الغرفة تلتف على نفسها عموديا"explanation="level editor, the top and bottom edges of the room are connected with each other. The player can still go to other rooms on the left and right"max="38*3"max_local="38*3"/>
<stringenglish="Room warping disabled"translation="الغرفة لا تلتف على نفسها"explanation="level editor, no edges are connected and the player can go to other rooms"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: No checkpoint to spawn at"translation="خطأ: لا نقطة حفظ يبعث اللاعب منها"explanation="we cannot playtest because there is no checkpoint in this room that the player could start (be spawned) at"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Max number of trinkets is 100"translation="خطأ: أقصى عدد للمقتنيات 100"explanation="editor, user tried to place another trinket"max="38*3"max_local="38*3"/>
<stringenglish="ERROR: Max number of crewmates is 100"translation="خطأ: أقصى عدد لأفراد الطاقم 100"explanation="editor, user tried to place another crewmate"max="38*3"max_local="38*3"/>
<stringenglish="Level quits to menu"translation="يغادر المستوى إلى القائمة"explanation="editor message, user would have been forcefully returned to title screen but wasn't"max="38*3"max_local="38*3"/>
<stringenglish="Level completed"translation="يختم المستوى"explanation="editor message, user would have been returned to levels list but wasn't"max="38*3"max_local="38*3"/>
<stringenglish="Rolled credits"translation="تظهر شاشة فريق العمل"explanation="editor message, credits would have been shown but weren't"max="38*3"max_local="38*3"/>
<stringenglish="Time trial completed"translation="تظهر شاشة ختم التحدي ضد الساعة"explanation="editor message, time trial complete screen would have been shown but wasn't"max="38*3"max_local="38*3"/>
<stringenglish="{hrs}:{min|digits=2}:{sec|digits=2}"translation="{hrs}:{min|digits=2}:{sec|digits=2}"explanation="time format H:MM:SS"/>
<stringenglish="{hrs}:{min|digits=2}:{sec|digits=2}.{cen|digits=2}"translation="{hrs}:{min|digits=2}:{sec|digits=2}.{cen|digits=2}"explanation="time format H:MM:SS.CC"/>
<stringenglish="{min}:{sec|digits=2}"translation="{min}:{sec|digits=2}"explanation="time format M:SS"/>
<stringenglish="{min}:{sec|digits=2}.{cen|digits=2}"translation="{min}:{sec|digits=2}.{cen|digits=2}"explanation="time format M:SS.CC"/>
<stringenglish="{sec}.{cen|digits=2}"translation="{sec}.{cen|digits=2}"explanation="time format S.CC"/>
<stringenglish=".99"translation=".99"explanation="appended to time format for 99/100 seconds (example: 1:15.99). Time trial results"/>
<stringenglish="{area}, {time}"translation="{area} / {time}"explanation="saved game summary, e.g. `Space Station, 12:30:59`"/>
<stringenglish="Level Complete!"translation="ختمت المستوى!"explanation="Might be tight, the exclamation mark may be removed"max="18"max_local="18"/>
<stringenglish="Game Complete!"translation="ختمت اللعبة!"explanation="Might be tight, the exclamation mark may be removed"max="18"max_local="18"/>
<stringenglish="You have rescued a crew member!"translation="أنقذت أحد أفراد طاقمك!"explanation="If you need to manually wordwrap: please ensure this has exactly two lines. Ignore the (font-adapted) maximum if it says 1 line."max="30*2"max_local="30*2"/>
<stringenglish="All Crew Members Rescued!"translation="أنقذت جميع طاقمك!"explanation=""max="32"max_local="32"/>
<stringenglish="All crewmates rescued!"translation="أنقذت جميع طاقمك!"explanation=""max="32"max_local="32"/>
<stringenglish="Press arrow keys or WASD to move"translation="تضغط الأسهم أو WASD للحركة"explanation=""max="32*2"max_local="32*2"/>
<stringenglish="Press left/right to move"translation="نضغط يسار/يمين للحركة"explanation=""max="32*2"max_local="32*2"/>
<stringenglish="Press {button} to flip"translation="نضغط {button} للعكس"explanation="expect `ACTION`"max="32*3"max_local="32*3"/>
<stringenglish="Press {button} to view map and quicksave"translation="نضغط {button} لرؤية الخريطة والتخزين السريع"explanation=""max="32*3"max_local="32*3"/>
<stringenglish="If you prefer, you can press UP or DOWN instead of ACTION to flip."translation="لو شئت، يمكنك للعكس ضغط فوق/تحت عوضا عن زر الفعل."explanation=""max="34*3"max_local="34*3"/>
<stringenglish="Help! Can anyone hear this message?"translation="النجدة!! هل من سميع لهذه الرسالة؟"explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Verdigris? Are you out there? Are you ok?"translation="فارديغري؟ أهذا أنت في الخارج؟ هل صحتك بخير؟"explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Please help us! We've crashed and need assistance!"translation="رجاء أنجدونا! سقطت سفينتنا ونحتاج مدد الدعم!"explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Hello? Anyone out there?"translation="ألو، ألو؟ هل من أحد يسمعني؟"explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="This is Doctor Violet from the D.S.S. Souleye! Please respond!"translation="هنا الدكتورة فايولت، من سفينة D.S.S. سولاي! رجاء أجيبوني!"explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Please... Anyone..."translation="ليت أحدا يرد... أترجاكم..."explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Please be alright, everyone..."translation="أن تعودوا جميعا... سالمين..."explanation="Violet speaking via Comms Relay"max="25*4"max_local="25*4"/>
<stringenglish="Congratulations!
You have found a shiny trinket!"translation="تهانينا!
وجدت تذكارا لماعا!"explanation=""max="34*4"max_local="34*4"/>
<stringenglish="Congratulations!
You have found a lost crewmate!"translation="تهانينا!
وجدت أحد أفراد الطاقم التائهين!"explanation=""max="34*4"max_local="34*4"/>
<stringenglish="Congratulations!
You have found the secret lab!"translation="تهانينا!
وجدت المخبر السري!"explanation=""max="34*4"max_local="34*4"/>
<stringenglish="The secret lab is separate from the rest of the game. You can now come back here at any time by selecting the new SECRET LAB option in the play menu."translation="المخبر السري جزء معزول عن بقية اللعبة. يمكنك العودة متى شئت بخيار مخصص له سيضاف في قائمة اللعب."explanation=""max="36*10"max_local="36*10"/>
<stringenglish="Viridian"translation="فيريديان"explanation="crewmate name (player)"max="15"max_local="15"/>
<stringenglish="Vitellary"translation="فيتيلاري"case="1"explanation="crewmate name as menu option: Who do you want to play the level with?"/>
<stringenglish="Vermilion"translation="فارميليون"case="1"explanation="crewmate name as menu option: Who do you want to play the level with?"/>
<stringenglish="Verdigris"translation="فارديغري"case="1"explanation="crewmate name as menu option: Who do you want to play the level with?"/>
<stringenglish="Victoria"translation="فيكتوريا"case="1"explanation="crewmate name as menu option: Who do you want to play the level with?"/>
<stringenglish="Starring"translation="بطولة"explanation="credits roll. Starring the following 6 crew members"max="20"max_local="20"/>
<stringenglish="Captain Viridian"translation="القبطان فيريديان"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="Doctor Violet"translation="الدكتورة فايوليت"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="Professor Vitellary"translation="الأستاذ فيتيلاري"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="Officer Vermilion"translation="النقيب فارميليون"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="Chief Verdigris"translation="القائد فارديغري"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="Doctor Victoria"translation="الدكتورة فيكتوريا"explanation="credits roll. Starring the following 6 crew members"max="27"max_local="27"/>
<stringenglish="When you're standing on the floor, Vitellary will try to walk to you."translation="أثناء وقوفك على الأرضية، سيحاول فيتيلاري المشي نحوك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the floor, Vermilion will try to walk to you."translation="أثناء وقوفك على الأرضية، سيحاول فارميليون المشي نحوك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the floor, Verdigris will try to walk to you."translation="أثناء وقوفك على الأرضية، سيحاول فارديغري المشي نحوك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the floor, Victoria will try to walk to you."translation="أثناء وقوفك على الأرضية، ستحاول فيكتوريا المشي نحوك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the floor, your companion will try to walk to you."translation="أثناء وقوفك على الأرضية، سيحاول مرافقك أيا كان المشي نحوك."explanation="Intermission 1, unknown companion, normally impossible but reproducible in custom levels"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the ceiling, Vitellary will try to walk to you."translation="أثناء وقوفك على السقف، سيحاول فيتيلاري المشي نحوك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the ceiling, Vermilion will try to walk to you."translation="أثناء وقوفك على السقف، سيحاول فارميليون المشي نحوك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the ceiling, Verdigris will try to walk to you."translation="أثناء وقوفك على السقف، سيحاول فارديغري المشي نحوك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the ceiling, Victoria will try to walk to you."translation="أثناء وقوفك على السقف، ستحاول فيكتوريا المشي نحوك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're standing on the ceiling, your companion will try to walk to you."translation="أثناء وقوفك على السقف، سيحاول مرافقك أيا كان المشي نحوك."explanation="Intermission 1 in flip mode, unknown companion, normally impossible but reproducible in custom levels"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the floor, Vitellary will stop and wait for you."translation="إن كفيت عن ملامسة الأرضية، سيتوقف فيتيلاري وينتظرك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the floor, Vermilion will stop and wait for you."translation="إن كفيت عن ملامسة الأرضية، سيتوقف فارميليون وينتظرك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the floor, Verdigris will stop and wait for you."translation="إن كفيت عن ملامسة الأرضية، سيتوقف فارديغري وينتظرك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the floor, Victoria will stop and wait for you."translation="إن كفيت عن ملامسة الأرضية، ستتوقف فيكتوريا وتنتظرك."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the floor, your companion will stop and wait for you."translation="إن كفيت عن ملامسة الأرضية، سيتوقف مرافقك أيا كان وينتظرك."explanation="Intermission 1, unknown companion, normally impossible but reproducible in custom levels"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the ceiling, Vitellary will stop and wait for you."translation="إن كفيت عن ملامسة السقف، سيتوقف فيتيلاري وينتظرك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the ceiling, Vermilion will stop and wait for you."translation="إن كفيت عن ملامسة السقف، سيتوقف فارميليون وينتظرك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the ceiling, Verdigris will stop and wait for you."translation="إن كفيت عن ملامسة السقف، سيتوقف فارديغري وينتظرك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the ceiling, Victoria will stop and wait for you."translation="إن كفيت عن ملامسة السقف، ستتوقف فيكتوريا وتنتظرك."explanation="Intermission 1 in flip mode"max="34*4"max_local="34*4"/>
<stringenglish="When you're NOT standing on the ceiling, your companion will stop and wait for you."translation="إن كفيت عن ملامسة السقف، سيتوقف مرافقك أيا كان وينتظرك."explanation="Intermission 1 in flip mode, unknown companion, normally impossible but reproducible in custom levels"max="34*4"max_local="34*4"/>
<stringenglish="You can't continue to the next room until he is safely across."translation="لا يمكنك المواصلة للغرفة القادمة قبل أن يعبر سالما."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="You can't continue to the next room until she is safely across."translation="لا يمكنك المواصلة للغرفة القادمة قبل أن تعبر سالمة."explanation="Intermission 1"max="34*4"max_local="34*4"/>
<stringenglish="You can't continue to the next room until they are safely across."translation="لا يمكنك المواصلة للغرفة القادمة قبل أن تعبرا سالمين."explanation="Intermission 1, unknown companion, normally impossible but reproducible in custom levels"max="34*4"max_local="34*4"/>
<stringenglish="Survive for"translation="سأحاول الصمود"explanation="Gravitron. Line 1/2: Survive for 60 seconds!"max="20"max_local="20"/>
<stringenglish="60 seconds!"translation="لمدة 60 ثانية!"explanation="Gravitron. Line 2/2: Survive for 60 seconds!"max="20"max_local="20"/>
<stringenglish="Thanks for"translation="شكرا على"explanation="credits. Line 1/2: Thanks for playing!"max="20"max_local="20"/>
<stringenglish="playing!"translation="اللعب!"explanation="credits. Line 2/2: Thanks for playing!"max="20"max_local="20"/>
<stringenglish="SPACE STATION 1 MASTERED"translation="احترفت المحطة الفضائية 1"explanation="achievement/trophy title"max="38*2"max_local="38*2"/>
<stringenglish="LABORATORY MASTERED"translation="احترفت المخبر"explanation="achievement/trophy title - V rank in time trial"max="38*2"max_local="38*2"/>
<stringenglish="THE TOWER MASTERED"translation="احترفت البرج"explanation="achievement/trophy title - V rank in time trial"max="38*2"max_local="38*2"/>
<stringenglish="SPACE STATION 2 MASTERED"translation="احترفت المحطة الفضائية 2"explanation="achievement/trophy title - V rank in time trial"max="38*2"max_local="38*2"/>
<stringenglish="WARP ZONE MASTERED"translation="احترفت منطقة التنقيل"explanation="achievement/trophy title - V rank in time trial"max="38*2"max_local="38*2"/>
<stringenglish="FINAL LEVEL MASTERED"translation="احترفت المستوى الأخير"explanation="achievement/trophy title - V rank in time trial"max="38*2"max_local="38*2"/>
<stringenglish="Obtain a V Rank in this Time Trial"translation="حصلت على رتبة V في هذا التحدي ضد الساعة."explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Complete the game"translation="ختمت اللعبة"case="1"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="FLIP MODE COMPLETE"translation="ختم الوضع المعكوس"explanation="achievement/trophy title"max="38*2"max_local="38*2"/>
<stringenglish="Complete the game in flip mode"translation="ختمت اللعبة في الوضع المعكوس"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Win with less than 50 deaths"translation="ختمت اللعبة بأقل من 50 ميتة"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Win with less than 100 deaths"translation="ختمت اللعبة بأقل من 100 ميتة"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Win with less than 250 deaths"translation="ختمت اللعبة بأقل من 250 ميتة"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Win with less than 500 deaths"translation="ختمت اللعبة بأقل من 500 ميتة"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 5 seconds on the Super Gravitron"translation="صمدت 5 ثوان في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 10 seconds on the Super Gravitron"translation="صمدت 10 ثوان في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 15 seconds on the Super Gravitron"translation="صمدت 15 ثانية في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 20 seconds on the Super Gravitron"translation="صمدت 20 ثانية في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 30 seconds on the Super Gravitron"translation="صمدت 30 ثانية في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Last 1 minute on the Super Gravitron"translation="صمدت دقيقة في أتون الجاذبية الخارق"explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="MASTER OF THE UNIVERSE"translation="المحترف الكوني"explanation="achievement/trophy title - no death mode complete"max="38*2"max_local="38*2"/>
<stringenglish="Complete the game in no death mode"translation="ختمت اللعبة في تحدي بدون الموت."explanation="achievement/trophy description"max="38*2"max_local="38*2"/>
<stringenglish="Something went wrong, but we forgot the error message."translation="حصلت مشكلة، لكن نسينا كتابة رسالة خطأ مناسبة."explanation="the message that is printed in case the game detects an error but there's no error message set"max="38*6"max_local="38*6"/>
<stringenglish="Could not mount {path}: real directory doesn't exist"translation="فشل ربط المسار {path}: لا وجود للمجلد الحقيقي"explanation="mount: link/attach a directory (folder) in the filesystem into the game's filesystem so we can access it"max="38*6"max_local="38*6"/>
<stringenglish="Level {path} not found"translation="لا وجود للمرحلة {path}"explanation=""max="38*6"max_local="38*6"/>
<stringenglish="Error parsing {path}: {error}"translation="فشل فتح {path}: {error}"explanation="we tried to parse the level file, but failed"max="38*6"max_local="38*6"/>
<stringenglish="{filename} dimensions not exact multiples of {width} by {height}!"translation="الأبعاد في {filename} ليست مضاعفات صحيحة لقيم العرض {width} والارتفاع {height}!"explanation="filename is something like tiles.png, tiles2.png, etc. and width/height are something like 8, 32, etc.; this is used if the dimensions of a graphics file aren't an exact multiple of the given size (e.g. 8x8, 32x32, etc.)"max="38*6"max_local="38*6"/>
<stringenglish="ERROR: Could not write to language folder! Make sure there is no "lang" folder next to the regular saves."translation="خطأ: فشلت الكتابة نحو مجلد اللغة! احرص أن لا مجلد "lang" بالقرب من التخزينات العادية"explanation=""max="38*5"max_local="38*5"/>
<!-- Please read README.txt for information about the language files -->
<strings_pluralmax_local_for="8x10">
<stringenglish_plural="You rescued {n_crew|wordy} crewmates"english_singular="You rescued {n_crew|wordy} crewmate"explanation="These two strings are displayed underneath each other (1/2)"max="40"var="n_crew"expect="6"max_local="40">
<translationform="0"translation="لم ننقذ أي فرد من الطاقم"/>
<translationform="1"translation="أنقذت فردا من الطاقم"/>
<translationform="2"translation="أنقذت فردين من الطاقم"/>
<translationform="3"translation="أنقذت {n_crew|wordy} أفراد من الطاقم"/>
<translationform="11"translation="أنقذت {n_crew|wordy} فرد من الطاقم"/>
</string>
<stringenglish_plural="and found {n_trinkets|wordy} trinkets."english_singular="and found {n_trinkets|wordy} trinket."explanation="These two strings are displayed underneath each other (2/2)"max="38*2"var="n_trinkets"expect="20"max_local="38*2">
<translationform="0"translation="ولم نجد أي تذكار."/>
<stringenglish_plural="And you found {n_trinkets|wordy} trinkets."english_singular="And you found {n_trinkets|wordy} trinket."explanation="You rescued all the crewmates! And you found XX trinket(s)."max="38*3"var="n_trinkets"expect="20"max_local="38*3">
<translationform="0"translation="ولم نجد أي تذكار."/>
<stringenglish_plural="{n_crew|wordy} crewmates remain"english_singular="{n_crew|wordy} crewmate remains"explanation="Reminder: you can add |upper for an uppercase letter."max="38*2"var="n_crew"expect="100"max_local="38*2">
<translationform="0"translation="لم يبق أي فرد من الطاقم"/>
<translationform="1"translation="بقي فرد من الطاقم"/>
<translationform="2"translation="بقي فردان من الطاقم"/>
<translationform="3"translation="بقي {n_crew|wordy} أفراد من الطاقم"/>
<translationform="11"translation="بقي {n_crew|wordy} فردا من الطاقم"/>
</string>
<stringenglish_plural="{n_crew|wordy} remain"english_singular="{n_crew|wordy} remains"explanation="You have rescued a crew member! XX remain"max="32*2"var="n_crew"expect="100"max_local="32*2">
<translationform="0"translation="لم يبق أي فرد من الطاقم"/>
<translationform="1"translation="بقي فرد من الطاقم"/>
<translationform="2"translation="بقي فردان من الطاقم"/>
<translationform="3"translation="بقي {n_crew|wordy} أفراد من الطاقم"/>
<translationform="11"translation="بقي {n_crew|wordy} فردا من الطاقم"/>
<translationform="0"translation="أصعب غرفة (بدون ميتات)"/>
<translationform="1"translation="أصعب غرفة (ميتة واحدة)"/>
<translationform="2"translation="أصعب غرفة (ميتتان)"/>
<translationform="3"translation="أصعب غرفة ({n_deaths|wordy2} ميتات)"/>
<translationform="11"translation="أصعب غرفة ({n_deaths|wordy2} ميتة)"/>
</string>
<stringenglish_plural="{n} normal room names untranslated"english_singular="{n} normal room name untranslated"explanation="per-area counts for room name translator mode"max="38*4"var="n"expect="48"max_local="38*4">
<translationform="0"translation="لم يبق أي اسم غرفة لم يترجم"/>
<translationform="1"translation="اسم غرفة عادي لم يترجم"/>
<translationform="2"translation="اسما غرفتين عادينين لم يترجما"/>
<translationform="3"translation="لم يترجم {n|wordy} أسماء لغرف عادية"/>
<translationform="11"translation="لم يترجم {n|wordy} اسم لغرف عادية"/>
<dialoguespeaker="purple"english="Something has gone horribly wrong with the ship's teleporter!"translation="Ha passat alguna cosa terrible amb el teletransportador de la nau!"/>
<dialoguespeaker="purple"english="I think everyone has been teleported away randomly! They could be anywhere!"translation="Em sembla que ha teletransportat tothom a llocs aleatoris! Podrien ser en qualsevol lloc!"/>
<dialoguespeaker="purple"english="I'm on the ship - it's damaged badly, but it's still intact!"translation="Jo sóc a la nau. Està molt malmesa,|però encara està sencera!"/>
<dialoguespeaker="purple"english="Where are you, Captain?"translation="Tu on ets, cap?"/>
<dialoguespeaker="cyan"english="I'm on some sort of space station... It seems pretty modern..."translation="En alguna mena d’estació espacial...|Sembla força moderna..."/>
<dialoguespeaker="purple"english="There seems to be some sort of interference in this dimension..."translation="Sembla que en aquesta dimensió hi ha alguna mena d’interferència..."/>
<dialoguespeaker="purple"english="I'm broadcasting the coordinates of the ship to you now."translation="Ara t’enviaré les coordenades de la nau."/>
<dialoguespeaker="purple"english="I can't teleport you back, but..."translation="No et puc teletransportar fins aquí, però..."/>
<dialoguespeaker="purple"english="If YOU can find a teleporter anywhere nearby, you should be able to teleport back to me!"translation="Si aconsegueixes trobar tu un teletransportador en algun lloc a prop d’on ets, hauries de poder-te teletransportar fins a la nau!"/>
<dialoguespeaker="cyan"english="Ok! I'll try to find one!"translation="Entesos! Miraré de trobar-ne un!"/>
<dialoguespeaker="purple"english="I'll keep trying to find the rest of the crew..."translation="Jo continuaré mirant de contactar amb la resta de la tripulació..."/>
</cutscene>
<cutsceneid="trenchwarfare"explanation="player finds Trench Warfare trinket, if no trinkets found yet">
<dialoguespeaker="cyan"english="Ohh! I wonder what that is?"translation="Oooh! Què deu ser, aquella cosa?"/>
<dialoguespeaker="cyan"english="I probably don't really need it, but it might be nice to take it back to the ship to study..."translation="Suposo que no la necessito, però potser estaria bé dur-la a la nau per a estudiar-la..."/>
</cutscene>
<cutsceneid="newtrenchwarfare"explanation="player finds Trench Warfare trinket, if other trinket already found">
<dialoguespeaker="cyan"english="Oh! It's another one of those shiny things!"translation="Oh! Una altra cosa brillant d’aquelles!"/>
<dialoguespeaker="cyan"english="I probably don't really need it, but it might be nice to take it back to the ship to study..."translation="Suposo que no la necessito, però potser estaria bé dur-la a la nau per a estudiar-la..."/>
<dialoguespeaker="player"english="So, Doctor - have you any idea what caused the crash?"translation="I doncs, doctora... Tens cap idea de què ha fet que ens estavelléssim?"/>
<dialoguespeaker="purple"english="There's some sort of bizarre signal here that's interfering with our equipment..."translation="Hi ha alguna mena de senyal estrany|que provoca interferències als nostres sistemes..."/>
<dialoguespeaker="purple"english="It caused the ship to lose its quantum position, collapsing us into this dimension!"translation="Ha fet que la nau perdés la posició quàntica i ens ha endinsat en aquesta dimensió!"/>
<dialoguespeaker="purple"english="But I think we should be able to fix the ship and get out of here..."translation="Però em sembla que hauríem de poder reparar la nau i sortir d’aquí..."/>
<dialoguespeaker="purple"english="... as long as we can find the rest of the crew."translation="Sempre que trobem la resta de la tripulació, és clar..."/>
<dialoguespeaker="purple"english="We really don't know anything about this place..."translation="No en sabem absolutament res, d’aquest lloc..."/>
<dialoguespeaker="purple"english="Our friends could be anywhere - they could be lost, or in danger!"translation="Els nostres amics podrien ser en qualsevol racó. I podrien estar perduts o en perill!"/>
<dialoguespeaker="player"english="Can they teleport back here?"translation="Es poden teletransportar fins aquí?"/>
<dialoguespeaker="purple"english="Not unless they find some way to communicate with us!"translation="Si no troben cap manera de comunicar-se amb nosaltres, no!"/>
<dialoguespeaker="purple"english="We can't pick up their signal and they can't teleport here unless they know where the ship is..."translation="No rebem cap senyal seu i no es poden teletransportar fins aquí si no saben on és la nau..."/>
<dialoguespeaker="player"english="So what do we do?"translation="I què fem, doncs?"/>
<dialoguespeaker="purple"english="We need to find them! Head out into the dimension and look for anywhere they might have ended up..."translation="Hem de trobar-los! Surt a la dimensió i mira de descobrir on han anat a parar..."/>
<dialoguespeaker="player"english="Ok! Where do we start?"translation="D’acord! Per on comencem?"/>
<dialoguespeaker="purple"english="Well, I've been trying to find them with the ship's scanners!"translation="Bé, jo he mirat de localitzar-los amb els escàners de la nau!"/>
<dialoguespeaker="purple"english="It's not working, but I did find something..."translation="No ha funcionat, però he descobert una cosa..."/>
<dialoguespeaker="purple"english="These points show up on our scans as having high energy patterns!"translation="Aquests punts que apareixen a l’escaneig tenen patrons d’alta energia!"/>
<dialoguespeaker="purple"english="There's a good chance they're teleporters - which means they're probably built near something important..."translation="És força probable que siguin teletransportadors, i això vol dir que estan construïts al costat de coses importants..."/>
<dialoguespeaker="purple"english="They could be a very good place to start looking."translation="Poden ser un molt bon lloc on començar a investigar."/>
<dialoguespeaker="player"english="Ok! I'll head out and see what I can find!"translation="D’acord! Doncs me’n vaig a mirar què hi trobo!"/>
<dialoguespeaker="purple"english="I'll be right here if you need any help!"translation="Si necessites ajuda, seré aquí!"/>
</cutscene>
<cutsceneid="bigopenworldskip"explanation="">
<dialoguespeaker="purple"english="I'll be right here if you need any help!"translation="Si necessites ajuda, seré aquí!"/>
</cutscene>
<cutsceneid="talkpurple_intro"explanation="">
<dialoguespeaker="player"english="I'm feeling a bit overwhelmed, Doctor."translation="Tot això m’aclapara una mica, doctora."/>
<dialoguespeaker="player"english="Where do I begin?"translation="Per on començo?"/>
<dialoguespeaker="purple"english="Remember that you can press {b_map} to check where you are on the map!"translation="Recorda que pots prémer {b_map} per a veure on ets del mapa!"buttons="1"/>
<dialoguespeaker="purple"english="Look for areas where the rest of the crew might be..."translation="Cerca zones on pugui haver-hi altres tripulants..."/>
<dialoguespeaker="purple"english="If you get lost, you can get back to the ship from any teleporter."translation="Si et perds, pots tornar a la nau a partir de qualsevol teletransportador."/>
<dialoguespeaker="purple"english="And don't worry! We'll find everyone!"translation="I no pateixis! Segur que trobarem tothom!"/>
<dialoguespeaker="purple"english="Everything will be ok!"translation="Tot anirà bé!"/>
</cutscene>
<cutsceneid="talkpurple_3"explanation="only one player string is shown">
<dialoguespeaker="purple"english="Are you doing ok, Captain?"translation="Estàs bé, cap?"/>
<dialoguespeaker="player"english="I'm worried about Victoria, Doctor!"translation="Em preocupa la Victòria, doctora!"/>
<dialoguespeaker="player"english="I'm worried about Vitellary, Doctor!"translation="Em preocupa en Vitel·lí, doctora!"/>
<dialoguespeaker="player"english="I'm worried about Verdigris, Doctor!"translation="Em preocupa en Verdet, doctora!"/>
<dialoguespeaker="player"english="I'm worried about Vermilion, Doctor!"translation="Em preocupa en Vermelló, doctora!"/>
<dialoguespeaker="player"english="I'm worried about you, Doctor!"translation="Em preocupes tu, doctora!"/>
<dialoguespeaker="purple"english="Oh - well, don't worry, they'll show up!"translation="Ah, no pateixis, segur que apareixerà!"/>
<dialoguespeaker="purple"english="Here! Have a lollipop!"translation="Té, una piruleta!"/>
</cutscene>
<cutsceneid="trinketcollector"explanation="if no trinkets found yet">
<dialoguespeaker="cyan"english="This seems like a good place to store anything I find out there..."translation="Això sembla un bon lloc on emmagatzemar les coses que trobi allà fora..."/>
<dialoguespeaker="cyan"english="Victoria loves to study the interesting things we find on our adventures!"translation="La Victòria gaudeix estudiant les coses interessants que trobem durant les nostres aventures!"/>
</cutscene>
<cutsceneid="newtrinketcollector"explanation="if at least one trinket found">
<dialoguespeaker="cyan"english="This seems like a good place to store those shiny things."translation="Això sembla un bon lloc on emmagatzemar aquelles coses brillants..."/>
<dialoguespeaker="cyan"english="Victoria loves to study the interesting things we find on our adventures!"translation="La Victòria gaudeix estudiant les coses interessants que trobem durant les nostres aventures!"/>
</cutscene>
<cutsceneid="new2trinketcollector"explanation="">
<dialoguespeaker="cyan"english="I hope she's ok..."translation="Espero que estigui bé..."/>
</cutscene>
<cutsceneid="rescuegreen"explanation="nuance: `she's BACK on the ship` means more `she's home` and not `she has returned to the ship`. She has never left the ship">
<dialoguespeaker="green"english="Captain! I've been so worried!"translation="Cap! Estava molt preocupat!"/>
<dialoguespeaker="player"english="Chief Verdigris! You're ok!"translation="Enginyer en cap Verdet! Estàs bé!"/>
<dialoguespeaker="green"english="I've been trying to get out, but I keep going around in circles..."translation="He mirat de sortir d’aquí, però no deixo de fer voltes..."/>
<dialoguespeaker="player"english="I've come from the ship. I'm here to teleport you back to it."translation="Vinc de la nau. Sóc aquí per a teletransportar-t’hi."/>
<dialoguespeaker="green"english="Is everyone else alright? Is Violet..."translation="La resta estan bé? I la Violeta..."/>
<dialoguespeaker="player"english="She's fine - she's back on the ship!"translation="Està bé. És a la nau!"/>
<dialoguespeaker="green"english="Oh! Great - Let's get going, then!"translation="Ah, fantàstic! Som-hi, doncs!"/>
</cutscene>
<cutsceneid="rescueblue"explanation="">
<dialoguespeaker="blue"english="Oh no! Captain! Are you stuck here too?"translation="Ostres, no! Cap! Tu tampoc no pots sortir d’aquí?"/>
<dialoguespeaker="player"english="It's ok - I'm here to rescue you!"translation="No pateixis, he vingut|a rescatar-te!"/>
<dialoguespeaker="player"english="Let me explain everything..."translation="Deixa’m explicar-t’ho tot..."/>
<dialoguespeaker="blue"english="What? I didn't understand any of that!"translation="Què? No he entès res de res!"/>
<dialoguespeaker="player"english="Oh... well, don't worry."translation="Ah... Bé, doncs no pateixis."/>
<dialoguespeaker="player"english="Follow me! Everything will be alright!"translation="Segueix-me! Tot anirà bé!"/>
<dialoguespeaker="red"english="Am I ever glad to see you! I thought I was the only one to escape the ship..."translation="M’alegro molt de veure’t! Pensava que era l’únic que s’havia escapat de la nau..."/>
<dialoguespeaker="player"english="Vermilion! I knew you'd be ok!"translation="Vermelló! Sabia que estaries bé!"/>
<dialoguespeaker="red"english="So, what's the situation?"translation="I doncs, quina és la situació?"/>
<dialoguespeaker="red"english="I see! Well, we'd better get back then."translation="És clar! D’acord, doncs millor que tornem."/>
<dialoguespeaker="red"english="There's a teleporter in the next room."translation="Hi ha un teletransportador a la sala del costat."/>
</cutscene>
<cutsceneid="rescueyellow"explanation="">
<dialoguespeaker="yellow"english="Ah, Viridian! You got off the ship alright too?"translation="Ah, Viridis! Tu també vas sortir de la nau sense fer-te mal, oi?"/>
<dialoguespeaker="player"english="It's good to see you're alright, Professor!"translation="M’alegro de veure que estàs bé, professor!"/>
<dialoguespeaker="yellow"english="Is the ship ok?"translation="La nau està bé?"/>
<dialoguespeaker="player"english="It's badly damaged, but Violet's been working on fixing it."translation="Està força malmesa, però la Violeta|treballa per a reparar-la."/>
<dialoguespeaker="player"english="We could really use your help..."translation="La teva ajuda ens vindria molt bé..."/>
<dialoguespeaker="yellow"english="Ah, of course!"translation="Ah, és clar!"/>
<dialoguespeaker="yellow"english="The background interference in this dimension prevented the ship from finding a teleporter when we crashed!"translation="Les interferències del rerefons d’aquesta dimensió van impedir que la nau trobés un teletransportador abans que ens estavelléssim!"/>
<dialoguespeaker="yellow"english="We've all been teleported to different locations!"translation="Per això hem acabat teletransportats|a diferents llocs!"/>
<dialoguespeaker="player"english="Er, that sounds about right!"translation="Eh... És clar, té sentit!"/>
<dialoguespeaker="yellow"english="Let's get back to the ship, then!"translation="Tornem a la nau, doncs!"/>
<dialoguespeaker="player"english="Something's gone wrong... We should look for a way back!"translation="Alguna cosa no ha anat bé... Hem de cercar un camí de tornada!"/>
</cutscene>
<cutsceneid="int1blue_2"explanation="">
<dialoguespeaker="player"english="Follow me! I'll help you!"translation="Segueix-me! T’ajudaré!"/>
<dialoguespeaker="blue"english="Promise you won't leave without me!"translation="Promet-me que no te n’aniràs sense mi!"/>
<dialoguespeaker="player"english="I promise! Don't worry!"translation="T’ho prometo! No pateixis!"/>
</cutscene>
<cutsceneid="int1blue_3"explanation="">
<dialoguespeaker="player"english="Are you ok down there, Doctor?"translation="Va tot bé allà baix, doctora?"/>
<dialoguespeaker="blue"english="I wanna go home!"translation="Me’n vull anar a casa!"/>
<dialoguespeaker="blue"english="Where are we? How did we even get here?"translation="On som? Com se suposa que hi hem arribat?"/>
<dialoguespeaker="player"english="Well, Violet did say that the interference in the dimension we crashed in was causing problems with the teleporters..."translation="Doncs... la Violeta deia que les interferències de la dimensió on ens hem estavellat causaven problemes amb els teletransportadors..."/>
<dialoguespeaker="player"english="I guess something went wrong..."translation="Suposo que alguna cosa ha anat malament..."/>
<dialoguespeaker="player"english="But if we can find another teleporter, I think we can get back to the ship!"translation="Però si trobem un altre teletransportador, suposo que podrem tornar a la nau!"/>
<dialoguespeaker="blue"english="Captain! Captain! Wait for me!"translation="Cap! Cap! Espera’m!"/>
<dialoguespeaker="blue"english="Please don't leave me behind! I don't mean to be a burden!"translation="No em deixis enrere!|No vull ser cap càrrega!"/>
<dialoguespeaker="player"english="Oh... don't worry Victoria, I'll look after you!"translation="Ah... No pateixis, Victòria, tindré cura de tu!"/>
</cutscene>
<cutsceneid="int1blue_5"explanation="">
<dialoguespeaker="blue"english="We're never going to get out of here, are we?"translation="No sortirem mai d’aquí, oi que no?"/>
<dialoguespeaker="player"english="I.. I don't know..."translation="N... no ho sé..."/>
<dialoguespeaker="player"english="I don't know where we are or how we're going to get out..."translation="No sé on som ni com n’hem de sortir..."/>
</cutscene>
<cutsceneid="int1blue_6"explanation="">
<dialoguespeaker="blue"english="We're going to be lost forever!"translation="Acabarem perduts per sempre més!"/>
<dialoguespeaker="player"english="Ok, come on... Things aren't that bad."translation="Au, va... Les coses no estan tan malament."/>
<dialoguespeaker="player"english="I have a feeling that we're nearly home!"translation="Tinc la sensació que som a prop de casa!"/>
<dialoguespeaker="player"english="We can't be too far from another teleporter!"translation="No podem ser gaire lluny d’un altre teletransportador!"/>
<dialoguespeaker="blue"english="I hope you're right, captain..."translation="Espero que tinguis raó, cap..."/>
</cutscene>
<cutsceneid="int1blue_7"explanation="">
<dialoguespeaker="blue"english="Captain! You were right! It's a teleporter!"translation="Cap! Tenies raó! Hi ha un teletransportador!"/>
<dialoguespeaker="player"english="Phew! You had me worried for a while there... I thought we were never going to find one."translation="Buf! Ja patia, la veritat... Pensava que no en trobaríem mai cap."/>
<dialoguespeaker="blue"english="What? Really?"translation="Què? De debò?"/>
<dialoguespeaker="player"english="Anyway, let's go back to the ship."translation="En fi, tornem a la nau."/>
</cutscene>
<cutsceneid="int1green_1"explanation="">
<dialoguespeaker="green"english="Huh? This isn't the ship..."translation="Eh? Això no és la nau..."/>
<dialoguespeaker="green"english="Captain! What's going on?"translation="Cap! Què passa?"/>
<dialoguespeaker="player"english="I... I don't know!"translation="N... no ho sé!"/>
<dialoguespeaker="player"english="Where are we?"translation="On som?"/>
<dialoguespeaker="green"english="Uh oh, this isn't good... Something must have gone wrong with the teleporter!"translation="Caram, això no va bé...|El teletransportador devia tenir algun problema!"/>
<dialoguespeaker="player"english="Ok... no need to panic!"translation="D’acord... Mantinguem la calma!"/>
<dialoguespeaker="player"english="Let's look for another teleporter!"translation="Cerquem un altre teletransportador!"/>
</cutscene>
<cutsceneid="int1green_2"explanation="">
<dialoguespeaker="player"english="Let's go this way!"translation="Anem cap allà!"/>
<dialoguespeaker="green"english="After you, Captain!"translation="Després de tu, cap!"/>
</cutscene>
<cutsceneid="int1green_3"explanation="just like in rescuegreen, `Violet is back on the ship` does not mean she was ever off the ship and has *returned* to it. Or maybe Verdigris thinks Violet was warped off the ship as well just like him and she *has* made her way back to the ship?">
<dialoguespeaker="green"english="So Violet's back on the ship? She's really ok?"translation="Així, la Violeta és a la nau? De debò que està bé?"/>
<dialoguespeaker="player"english="She's fine! She helped me find my way back!"translation="I tant! M’ha ajudat a trobar|el camí de tornada!"/>
<dialoguespeaker="green"english="Oh, phew! I was worried about her."translation="Ah, uf! Patia per ella."/>
<dialoguespeaker="green"english="Captain, I have a secret..."translation="Cap, tinc un secret..."/>
<dialoguespeaker="green"english="I really like Violet!"translation="M’agrada molt la Violeta!"/>
<dialoguespeaker="player"english="Is that so?"translation="De debò?"/>
<dialoguespeaker="green"english="Please promise you won't tell her!"translation="Promet-me que no li ho diràs!"/>
<dialoguespeaker="player"english="Are you doing ok?"translation="Va tot bé?"/>
<dialoguespeaker="green"english="I think so! I really hope we can find a way back to the ship..."translation="Diria que sí! Espero que trobem|un camí cap a la nau..."/>
</cutscene>
<cutsceneid="int1green_5"explanation="">
<dialoguespeaker="green"english="So, about Violet..."translation="Daixò... Pel que fa a la Violeta..."/>
<dialoguespeaker="player"english="So, do you think you'll be able to fix the ship?"translation="I què, creus que podràs|reparar la nau?"/>
<dialoguespeaker="green"english="Depends on how bad it is... I think so, though!"translation="Depèn de com estigui de malament...|Però penso que sí!"/>
<dialoguespeaker="green"english="It's not very hard, really. The basic dimensional warping engine design is pretty simple, and if we can get that working we shouldn't have any trouble getting home."translation="No és tan difícil, en realitat. El disseny bàsic del motor de salt dimensional és força simple,|i si aconseguim que això funcioni, no hauríem de tenir problemes per a arribar a casa."/>
<dialoguespeaker="green"english="Finally! A teleporter!"translation="Per fi! Un teletransportador!"/>
<dialoguespeaker="green"english="I was getting worried we wouldn't find one..."translation="Ja començava a pensar que no en trobaríem cap..."/>
<dialoguespeaker="player"english="Let's head back to the ship!"translation="Tornem a la nau!"/>
</cutscene>
<cutsceneid="int1red_1"explanation="">
<dialoguespeaker="red"english="Wow! Where are we?"translation="Bufa! On som?"/>
<dialoguespeaker="player"english="This... isn't right... Something must have gone wrong with the teleporter!"translation="Això... no va bé... El teletransportador devia tenir algun problema!"/>
<dialoguespeaker="red"english="Oh well... We can work it out when we get back to the ship!"translation="Què hi farem... Ja ho investigarem quan tornem a la nau!"/>
<dialoguespeaker="red"english="Let's go exploring!"translation="Som-hi, a explorar!"/>
<dialoguespeaker="red"english="Hey Viridian... how did the crash happen, exactly?"translation="Ei, Viridis... Com és que ens vam estavellar, exactament?"/>
<dialoguespeaker="player"english="Oh, I don't really know - some sort of interference..."translation="Ah, doncs no ho sé del cert... Només sé que per les interferències..."/>
<dialoguespeaker="player"english="...or something sciencey like that. It's not really my area."translation="O per alguna cosa científica així... No és el meu fort, aquest tema."/>
<dialoguespeaker="red"english="Ah! Well, do you think we'll be able to fix the ship and go home?"translation="Ah! Bé, i creus que podrem reparar la nau i tornar a casa?"/>
<dialoguespeaker="player"english="Of course! Everything will be ok!"translation="És clar! Tot anirà bé!"/>
<dialoguespeaker="red"english="Hi again! You doing ok?"translation="Ep! Va tot bé?"/>
<dialoguespeaker="player"english="I think so! But I really want to get back to the ship..."translation="Crec que sí! Però tinc moltes ganes de tornar a la nau..."/>
<dialoguespeaker="red"english="We'll be ok! If we can find a teleporter somewhere we should be able to get back!"translation="Ens en sortirem! Si aconseguim trobar un teletransportador en algun lloc, hauríem de poder tornar-hi!"/>
</cutscene>
<cutsceneid="int1red_5"explanation="">
<dialoguespeaker="red"english="Are we there yet?"translation="Falta gaire?"/>
<dialoguespeaker="player"english="We're getting closer, I think..."translation="Ens hi estem acostant, em sembla..."/>
<dialoguespeaker="player"english="I wonder where we are, anyway?"translation="On devem ser, a tot això?"/>
<dialoguespeaker="player"english="This seems different from that dimension we crashed in, somehow..."translation="No sé per què, però aquest lloc sembla diferent de la dimensió on ens vam estavellar..."/>
<dialoguespeaker="red"english="I dunno... But we must be close to a teleporter by now..."translation="No sé... Però ara ja devem ser a prop d’un teletransportador..."/>
</cutscene>
<cutsceneid="int1red_7"explanation="">
<dialoguespeaker="player"english="We're there!"translation="Ja hi som!"/>
<dialoguespeaker="red"english="See? I told you! Let's get back to the ship!"translation="Ho veus? T’ho he dit! Vinga, tornem a la nau!"/>
</cutscene>
<cutsceneid="int1yellow_1"explanation="">
<dialoguespeaker="yellow"english="Oooh! This is interesting..."translation="Oooh! Que interessant..."/>
<dialoguespeaker="yellow"english="Captain! Have you been here before?"translation="Cap! Has estat mai aquí?"/>
<dialoguespeaker="player"english="What? Where are we?"translation="Què? On som?"/>
<dialoguespeaker="yellow"english="I suspect something deflected our teleporter transmission! This is somewhere new..."translation="Em sembla que alguna cosa ha desviat la transmissió del teletransportador! Aquest lloc és nou..."/>
<dialoguespeaker="player"english="We should try to find a teleporter and get back to the ship..."translation="Hauríem de mirar de trobar un teletransportador i tornar a la nau..."/>
<dialoguespeaker="player"english="What do you make of all this, Professor?"translation="Què en penses, de|tot això, professor?"/>
<dialoguespeaker="yellow"english="I'm guessing this dimension has something to do with the interference that caused us to crash!"translation="Suposo que aquesta dimensió té|alguna cosa a veure amb les interferències que han causat que ens estavelléssim!"/>
<dialoguespeaker="yellow"english="Maybe we'll find the cause of it here?"translation="Potser en trobarem la causa aquí?"/>
<dialoguespeaker="player"english="Oh wow! Really?"translation="Ah, ostres! De debò?"/>
<dialoguespeaker="yellow"english="Well, it's just a guess. I'll need to get back to the ship before I can do any real tests..."translation="Bé, només és una suposició. Necessito tornar a la nau abans de poder fer cap experiment real..."/>
</cutscene>
<cutsceneid="int1yellow_4"explanation="the split paths merge, and Vitellary sees a checkpoint">
<dialoguespeaker="yellow"english="Ohh! What was that?"translation="Oooh! Què és, això?"/>
<dialoguespeaker="player"english="What was what?"translation="Què és, això?"/>
<dialoguespeaker="yellow"english="That big... C thing! I wonder what it does?"translation="Aquesta cosa grossa... amb una C! Què deu fer?"/>
<dialoguespeaker="player"english="Em... I don't really know how to answer that question..."translation="Eh... No sé com respondre a la teva qüestió..."/>
<dialoguespeaker="player"english="It's probably best not to acknowledge that it's there at all."translation="Segurament és millor que fem veure que no existeix."/>
<dialoguespeaker="yellow"english="Maybe we should take it back to the ship to study it?"translation="Ens la podríem emportar a la nau i estudiar-la!"/>
<dialoguespeaker="player"english="We really shouldn't think about it too much... Let's keep moving!"translation="No hi hauríem de pensar pas gaire... Vinga, som-hi!"/>
</cutscene>
<cutsceneid="int1yellow_5"explanation="">
<dialoguespeaker="yellow"english="You know, there's something really odd about this dimension..."translation="Saps? Hi ha una cosa molt estranya en aquesta dimensió..."/>
<dialoguespeaker="yellow"english="We shouldn't really be able to move between dimensions with a regular teleporter..."translation="No ens hauríem de poder moure entre dimensions amb un teletransportador normal..."/>
<dialoguespeaker="yellow"english="Maybe this isn't a proper dimension at all?"translation="Potser no és una dimensió com a tal?"/>
<dialoguespeaker="yellow"english="Maybe it's some kind of polar dimension? Something artificially created for some reason?"translation="Potser és alguna mena de dimensió polar? Alguna cosa creada artificialment per algun motiu..."/>
<dialoguespeaker="yellow"english="I can't wait to get back to the ship. I have a lot of tests to run!"translation="Em moro de ganes de tornar a la nau. Hi he de fer un munt d’experiments!"/>
</cutscene>
<cutsceneid="int1yellow_6"explanation="">
<dialoguespeaker="yellow"english="I wonder if there's anything else in this dimension worth exploring?"translation="Hi deu haver alguna altra cosa d’aquesta dimensió que pagui la pena d’explorar?"/>
<dialoguespeaker="player"english="Maybe... but we should probably just focus on finding the rest of the crew for now..."translation="Potser... Però segurament només ens hauríem de centrar a trobar la resta de la tripulació, ara mateix..."/>
<dialoguespeaker="purple"english="... I hope Verdigris is alright."translation="Espero que en Verdet estigui bé..."/>
<dialoguespeaker="purple"english="If you can find him, he'd be a big help fixing the ship!"translation="Si aconseguissis trobar-lo, seria de gran ajuda a l’hora|de reparar la nau!"/>
</cutscene>
<cutsceneid="talkpurple_2"explanation="">
<dialoguespeaker="purple"english="Chief Verdigris is so brave and ever so smart!"translation="L’enginyer en cap Verdet és valent i intel·ligent alhora!"/>
</cutscene>
<cutsceneid="talkpurple_4"explanation="">
<dialoguespeaker="purple"english="Welcome back, Captain!"translation="Tornes a ser aquí, cap!"/>
<dialoguespeaker="purple"english="I think Victoria is quite happy to be back on the ship."translation="Em sembla que la Victòria està força contenta de tornar a ser a la nau."/>
<dialoguespeaker="purple"english="She really doesn't like adventuring. She gets very homesick!"translation="No li agrada gens anar d’aventures. S’enyora molt fàcilment!"/>
</cutscene>
<cutsceneid="talkpurple_5"explanation="only one of the last 6 strings is shown">
<dialoguespeaker="purple"english="Vermilion called in to say hello!"translation="En Vermelló t’envia salutacions!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find the rest of the crew!"translation="Té moltes ganes d’ajudar-te a trobar la resta de la tripulació!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Victoria!"translation="Té moltes ganes d’ajudar-te a trobar la Victòria!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Vitellary!"translation="Té moltes ganes d’ajudar-te a trobar en Vitel·lí!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Verdigris!"translation="Té moltes ganes d’ajudar-te a trobar en Verdet!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find Vermilion!"translation="Té moltes ganes d’ajudar-te a trobar en Vermelló!"/>
<dialoguespeaker="purple"english="He's really looking forward to helping you find you!"translation="Té moltes ganes d’ajudar-te|a trobar-te!"/>
</cutscene>
<cutsceneid="talkpurple_6"explanation="">
<dialoguespeaker="purple"english="Captain! You found Verdigris!"translation="Cap! Has trobat en Verdet!"/>
<dialoguespeaker="purple"english="Thank you so much!"translation="Moltes gràcies!"/>
</cutscene>
<cutsceneid="talkpurple_7"explanation="">
<dialoguespeaker="purple"english="I'm glad Professor Vitellary is ok!"translation="M’alegro que el professor Vitel·lí estigui bé!"/>
<dialoguespeaker="purple"english="He had lots of questions for me about this dimension."translation="Em volia fer un munt de qüestions sobre aquesta dimensió."/>
<dialoguespeaker="purple"english="He's already gotten to work with his research!"translation="Ja s’ha posat a investigar!"/>
<dialoguespeaker="player"english="Doctor, something strange happened when we teleported back to the ship..."translation="Doctora, ha passat una cosa estranya quan ens hem teletransportat a la nau..."/>
<dialoguespeaker="player"english="We got lost in another dimension!"translation="Ens hem perdut en una altra dimensió!"/>
<dialoguespeaker="purple"english="Maybe that dimension has something to do with the interference that caused us to crash here?"translation="Potser aquella dimensió té alguna cosa a veure amb les interferències que han causat que ens estavelléssim aquí?"/>
<dialoguespeaker="purple"english="I'll look into it..."translation="Ho investigaré..."/>
<dialoguespeaker="player"english="Doctor! Doctor! It happened again!"translation="Doctora, doctora! Ha tornat a passar!"/>
<dialoguespeaker="player"english="The teleporter brought us to that weird dimension..."translation="El teletransportador ens ha portat a aquella dimensió estranya..."/>
<dialoguespeaker="purple"english="Hmm, there's definitely something strange happening..."translation="Hum... Sens dubte, passa alguna cosa estranya..."/>
<dialoguespeaker="purple"english="If only we could find the source of that interference!"translation="Tant de bo trobéssim l’origen de les interferències!"/>
<dialoguespeaker="player"english="Doctor, something strange has been happening when we teleport back to the ship..."translation="Doctora, quan ens teletransportem a la nau, passen coses estranyes..."/>
<dialoguespeaker="player"english="We keep getting brought to another weird dimension!"translation="Sempre ens acaba portant a una altra dimensió estranya!"/>
<dialoguespeaker="purple"english="Maybe that dimension has something to do with the interference that caused us to crash here?"translation="Potser aquella dimensió té alguna cosa a veure amb les interferències que han causat que ens estavelléssim aquí?"/>
<dialoguespeaker="purple"english="Hmm, there's definitely something strange happening..."translation="Hum... Sens dubte, passa alguna cosa estranya..."/>
<dialoguespeaker="purple"english="If only we could find the source of that interference!"translation="Tant de bo trobéssim l’origen de les interferències!"/>
</cutscene>
<cutsceneid="talkpurple_8"explanation="">
<dialoguespeaker="purple"english="Hey Captain! Now that you've turned off the source of the interference, we can warp everyone back to the ship instantly, if we need to!"translation="Ei, cap! Ara que has desactivat l’origen de les interferències, podem teletransportar tothom a la nau instantàniament, si cal!"/>
<dialoguespeaker="purple"english="Any time you want to come back to the ship, just select the new SHIP option in your menu!"translation="Si en algun moment vols tornar a la nau, selecciona la nova opció NAU al menú!"/>
</cutscene>
<cutsceneid="talkgreen_1"explanation="">
<dialoguespeaker="green"english="I'm an engineer!"translation="Sóc enginyer!"/>
</cutscene>
<cutsceneid="talkgreen_2"explanation="">
<dialoguespeaker="green"english="I think I can get this ship moving again, but it's going to take a while..."translation="Em sembla que podré aconseguir que la nau es torni a moure, però em costarà una bona estona..."/>
</cutscene>
<cutsceneid="talkgreen_3"explanation="">
<dialoguespeaker="green"english="Victoria mentioned something about a lab? I wonder if she found anything down there?"translation="La Victòria parlava d’un laboratori o una cosa així? Potser hi ha trobat alguna cosa, allà baix?"/>
</cutscene>
<cutsceneid="talkgreen_4"explanation="">
<dialoguespeaker="green"english="Vermilion's back! Yey!"translation="En Vermelló ha tornat! Visca!"/>
</cutscene>
<cutsceneid="talkgreen_5"explanation="">
<dialoguespeaker="green"english="The Professor had lots of questions about this dimension for me..."translation="El professor em volia fer un munt de qüestions sobre aquesta dimensió..."/>
<dialoguespeaker="green"english="We still don't really know that much, though."translation="Però encara no en sabem gaire cosa."/>
<dialoguespeaker="green"english="Until we work out what's causing that interference, we can't go anywhere."translation="Fins que no descobrim què causa les interferències, no podrem anar enlloc."/>
</cutscene>
<cutsceneid="talkgreen_6"explanation="">
<dialoguespeaker="green"english="I'm so glad that Violet's alright!"translation="M’alegro molt que la Violeta estigui bé!"/>
</cutscene>
<cutsceneid="talkgreen_7"explanation="">
<dialoguespeaker="green"english="That other dimension we ended up in must be related to this one, somehow..."translation="L’altra dimensió on havíem anat a parar deu estar relacionada amb aquesta d’alguna manera..."/>
</cutscene>
<cutsceneid="talkgreen_8"explanation="">
<dialoguespeaker="green"english="The antenna's broken! This is going to be very hard to fix..."translation="L’antena està trencada! Això costarà molt de reparar..."/>
</cutscene>
<cutsceneid="talkgreen_9"explanation="">
<dialoguespeaker="green"english="It looks like we were warped into solid rock when we crashed!"translation="Sembla com si ens haguéssim teletransportat enmig de roca sòlida en estavellar-nos!"/>
<dialoguespeaker="green"english="Hmm. It's going to be hard to separate from this..."translation="Hum... Costarà separar-nos-en..."/>
</cutscene>
<cutsceneid="talkgreen_10"explanation="">
<dialoguespeaker="green"english="The ship's all fixed up. We can leave at a moment's notice!"translation="La nau ja està tota reparada. Podem anar-nos-en en un tres i no res!"/>
<dialoguespeaker="red"english="We'll find a way out of here!"translation="Trobarem una manera de sortir d’aquí!"/>
</cutscene>
<cutsceneid="talkred_2"explanation="">
<dialoguespeaker="red"english="I hope Victoria is ok..."translation="Espero que la Victòria estigui bé..."/>
<dialoguespeaker="red"english="She doesn't handle surprises very well..."translation="No li agraden gaire les sorpreses..."/>
</cutscene>
<cutsceneid="talkred_3"explanation="">
<dialoguespeaker="red"english="I don't know how we're going to get this ship working again!"translation="No sé pas com farem que la nau torni a funcionar!"/>
<dialoguespeaker="red"english="Chief Verdigris would know what to do..."translation="L’enginyer en cap Verdet sabria què fer..."/>
</cutscene>
<cutsceneid="talkred_4"explanation="">
<dialoguespeaker="red"english="I wonder what caused the ship to crash here?"translation="Què devia fer que la nau s’estavellés aquí?"/>
<dialoguespeaker="red"english="It's the shame the Professor isn't here, huh? I'm sure he could work it out!"translation="És una llàstima que el professor no hi sigui, eh? Segur que ell ho esbrinaria!"/>
</cutscene>
<cutsceneid="talkred_5"explanation="">
<dialoguespeaker="red"english="It's great to be back!"translation="És genial tornar a ser aquí!"/>
<dialoguespeaker="red"english="I can't wait to help you find the rest of the crew!"translation="Em moro de ganes d’ajudar-te a trobar la resta de tripulants!"/>
<dialoguespeaker="red"english="It'll be like old times, huh, Captain?"translation="Serà com als vells temps, oi, cap?"/>
</cutscene>
<cutsceneid="talkred_6"explanation="">
<dialoguespeaker="red"english="It's good to have Victoria back with us."translation="És genial que la Victòria torni a ser amb nosaltres."/>
<dialoguespeaker="red"english="She really seems happy to get back to work in her lab!"translation="Sembla molt feliç de poder tornar a treballar al seu laboratori!"/>
</cutscene>
<cutsceneid="talkred_7"explanation="">
<dialoguespeaker="red"english="I think I saw Verdigris working on the outside of the ship!"translation="Em sembla que he vist en Verdet treballant a la part exterior de la nau!"/>
</cutscene>
<cutsceneid="talkred_8"explanation="">
<dialoguespeaker="red"english="You found Professor Vitellary! All right!"translation="Has trobat el professor Vitel·lí! Molt bé!"/>
<dialoguespeaker="red"english="We'll have this interference thing worked out in no time now!"translation="Ara resoldre el tema de les interferències serà bufar i fer ampolles!"/>
</cutscene>
<cutsceneid="talkred_9"explanation="">
<dialoguespeaker="red"english="That other dimension was really strange, wasn't it?"translation="Aquella altra dimensió era ben estranya, eh?"/>
<dialoguespeaker="red"english="I wonder what caused the teleporter to send us there?"translation="Per què el teletransportador ens va enviar allà?"/>
<dialoguespeaker="red"english="I found something interesting around here - the same warp signature I saw when I landed!"translation="En aquesta zona he trobat una cosa interessant: les mateixes formes derivades d’un teletransport que vaig veure quan vaig aterrar!"/>
<dialoguespeaker="red"english="Someone from the ship must be nearby..."translation="Hi ha d’haver algú de la nau a prop..."/>
</cutscene>
<cutsceneid="talkred_13"explanation="">
<dialoguespeaker="red"english="This dimension is pretty exciting, isn't it?"translation="Aquesta dimensió és força emocionant, oi?"/>
<dialoguespeaker="red"english="I wonder what we'll find?"translation="Què hi trobarem?"/>
</cutscene>
<cutsceneid="talkblue_1"explanation="">
<dialoguespeaker="blue"english="Any signs of Professor Vitellary?"translation="Hi ha cap senyal del professor Vitel·lí?"/>
<dialoguespeaker="player"english="Sorry, not yet..."translation="Em sap greu, però de moment, no..."/>
<dialoguespeaker="blue"english="I hope he's ok..."translation="Espero que estigui bé..."/>
</cutscene>
<cutsceneid="talkblue_2"explanation="">
<dialoguespeaker="blue"english="Thanks so much for saving me, Captain!"translation="Moltes gràcies per salvar-me, cap!"/>
</cutscene>
<cutsceneid="talkblue_3"explanation="">
<dialoguespeaker="blue"english="I'm so glad to be back!"translation="M’alegro molt de tornar a ser aquí!"/>
<dialoguespeaker="blue"english="That lab was so dark and scary! I didn't like it at all..."translation="Aquell laboratori era fosc i feia por! No m’agradava gens..."/>
</cutscene>
<cutsceneid="talkblue_4"explanation="">
<dialoguespeaker="blue"english="Vitellary's back? I knew you'd find him!"translation="En Vitel·lí torna a ser aquí? Sabia que el trobaries!"/>
<dialoguespeaker="blue"english="I mean, I admit I was very worried that you wouldn't..."translation="Tot i que he d’admetre que tenia molta por que no el trobessis..."/>
<dialoguespeaker="blue"english="or that something might have happened to him..."translation="O que li hagués passat|alguna cosa..."/>
<dialoguespeaker="player"english="Doctor Victoria? He's ok!"translation="Doctora Victòria... Que està bé!"/>
<dialoguespeaker="blue"english="Oh! Sorry! I was just thinking about what if he wasn't?"translation="Ah, perdona! És que pensava en què passaria si no ho estigués!"/>
<dialoguespeaker="blue"english="Though I was very worried..."translation="Tot i que patia molt..."/>
</cutscene>
<cutsceneid="talkblue_7"explanation="">
<dialoguespeaker="blue"english="Why did the teleporter send us to that scary dimension?"translation="Per què el teletransportador ens ha enviat a aquella dimensió tan espantosa?"/>
<dialoguespeaker="blue"english="What happened?"translation="Què ha passat?"/>
<dialoguespeaker="player"english="I don't know, Doctor..."translation="No ho sé, doctora..."/>
<dialoguespeaker="blue"english="Are you going to try and find the rest of these shiny things?"translation="Miraràs de trobar la resta d’aquestes coses brillants?"/>
</cutscene>
<cutsceneid="talkblue_trinket1"explanation="">
<dialoguespeaker="blue"english="Hey Captain, I found this in that lab..."translation="Ei, cap, he trobat això en aquell laboratori..."/>
<dialoguespeaker="blue"english="Any idea what it does?"translation="Tens cap idea de per a què serveix?"/>
<dialoguespeaker="player"english="Sorry, I don't know!"translation="Em sap greu, però no ho sé!"/>
<dialoguespeaker="player"english="Maybe something will happen if we find them all?"translation="Potser passarà alguna cosa si els trobem tots?"/>
</cutscene>
<cutsceneid="talkblue_trinket2"explanation="">
<dialoguespeaker="blue"english="Captain! Come have a look at what I've been working on!"translation="Cap! Mira en què he estat treballant!"/>
<dialoguespeaker="blue"english="It looks like these shiny things are giving off a strange energy reading!"translation="Sembla que aquelles coses brillants tenen unes lectures d’energia estranyes!"/>
<dialoguespeaker="blue"english="So I analysed it..."translation="Així que ho he analitzat..."/>
</cutscene>
<cutsceneid="talkblue_trinket3"explanation="">
<dialoguespeaker="blue"english="Captain! Come have a look at what I've been working on!"translation="Cap! Mira en què he estat treballant!"/>
<dialoguespeaker="blue"english="I found this in that lab..."translation="He trobat això en aquell laboratori..."/>
<dialoguespeaker="blue"english="It seemed to be giving off a weird energy reading..."translation="Tenia unes lectures d’energia estranyes..."/>
<dialoguespeaker="blue"english="So I analysed it..."translation="Així que ho he analitzat..."/>
</cutscene>
<cutsceneid="talkblue_trinket4"explanation="">
<dialoguespeaker="blue"english="...and I was able to find more of them with the ship's scanner!"translation="...i n’he descobert més amb l’escàner de la nau!"/>
<dialoguespeaker="blue"english="If you get a chance, it might be worth finding the rest of them!"translation="Si t’és possible, potser estaria|bé trobar-ne la resta!"/>
<dialoguespeaker="blue"english="Don't put yourself in any danger, though!"translation="Però no et posis en perill, eh?"/>
</cutscene>
<cutsceneid="talkblue_trinket5"explanation="">
<dialoguespeaker="blue"english="...but it looks like you've already found all of them in this dimension!"translation="...però sembla que ja les has trobades totes en aquesta dimensió!"/>
<dialoguespeaker="player"english="Oh? Really?"translation="Ah, sí? De debò?"/>
<dialoguespeaker="blue"english="Yeah, well done! That can't have been easy!"translation="Sí, molt ben fet! Segur que no ha estat fàcil!"/>
</cutscene>
<cutsceneid="talkblue_trinket6"explanation="">
<dialoguespeaker="blue"english="...and they're related. They're all a part of something bigger!"translation="...i estan relacionades. Són part d’una cosa més grossa!"/>
<dialoguespeaker="player"english="Oh? Really?"translation="Ah, sí? De debò?"/>
<dialoguespeaker="blue"english="Yeah! There seem to be twenty variations of the fundamental energy signature..."translation="Sí! Sembla que hi ha vint variacions de la signatura d’energia fonamental..."/>
<dialoguespeaker="blue"english="Does that mean you've found all of them?"translation="Això vol dir que les has trobades totes?"/>
</cutscene>
<cutsceneid="talkyellow_1"explanation="">
<dialoguespeaker="yellow"english="I'm making some fascinating discoveries, captain!"translation="Estic fent uns descobriments fascinants, cap!"/>
</cutscene>
<cutsceneid="talkyellow_2"explanation="">
<dialoguespeaker="yellow"english="This isn't like any other dimension we've been to, Captain."translation="Aquesta dimensió no és com les altres on hem estat, cap."/>
<dialoguespeaker="yellow"english="There's something strange about this place..."translation="Aquest lloc és estrany, per algun motiu..."/>
</cutscene>
<cutsceneid="talkyellow_3"explanation="">
<dialoguespeaker="yellow"english="Captain, have you noticed that this dimension seems to wrap around?"translation="Cap, t’has adonat que aquesta dimensió sembla que sigui cíclica? Quan en surts per un costat, apareixes per l’altre."/>
<dialoguespeaker="player"english="Yeah, it's strange..."translation="Sí, és estrany..."/>
<dialoguespeaker="yellow"english="It looks like this dimension is having the same stability problems as our own!"translation="Sembla com si aquesta dimensió tingués els mateixos problemes d’estabilitat que nosaltres!"/>
<dialoguespeaker="yellow"english="I hope we're not the ones causing it..."translation="Espero que no en siguem|els causants..."/>
<dialoguespeaker="player"english="What? Do you think we might be?"translation="Què? Creus que podem ser-ho?"/>
<dialoguespeaker="yellow"english="No no... that's very unlikely, really..."translation="No, no... És molt improbable, de fet..."/>
</cutscene>
<cutsceneid="talkyellow_4"explanation="">
<dialoguespeaker="yellow"english="My guess is that whoever used to live here was experimenting with ways to stop the dimension from collapsing."translation="Suposo que qui fos que vivia aquí experimentava amb maneres de fer que la dimensió no s’esfondrés."/>
<dialoguespeaker="yellow"english="It would explain why they've wrapped the edges..."translation="Això explicaria per què les vores esdevenen cícliques..."/>
<dialoguespeaker="yellow"english="Hey, maybe that's what's causing the interference?"translation="Escolta, potser és això, el que causa les interferències?"/>
</cutscene>
<cutsceneid="talkyellow_5"explanation="">
<dialoguespeaker="yellow"english="I wonder where the people who used to live here have gone?"translation="On deu haver anat, la gent que vivia aquí?"/>
</cutscene>
<cutsceneid="talkyellow_6"explanation="">
<dialoguespeaker="yellow"english="I think it's no coincidence that the teleporter was drawn to that dimension..."translation="Em penso que no és cap coincidència que el teletransportador anés a parar a aquella dimensió..."/>
<dialoguespeaker="yellow"english="There's something there. I think it might be causing the interference that's stopping us from leaving..."translation="Allà hi ha alguna cosa. Potser és el que causa les interferències que no ens permeten anar-nos-en..."/>
</cutscene>
<cutsceneid="talkyellow_7"explanation="">
<dialoguespeaker="yellow"english="I'm glad Verdigris is alright."translation="M’alegro que en Verdet estigui bé."/>
<dialoguespeaker="yellow"english="It'll be a lot easier to find some way out of here now that we can get the ship working again!"translation="Ara que podem fer que la nau torni a funcionar, serà molt més fàcil trobar una manera de sortir d’aquí!"/>
</cutscene>
<cutsceneid="talkyellow_8"explanation="">
<dialoguespeaker="yellow"english="Ah, you've found Doctor Victoria? Excellent!"translation="Ah, has trobat la doctora Victòria? Excel·lent!"/>
<dialoguespeaker="yellow"english="I have lots of questions for her!"translation="Li he de fer un munt de qüestions!"/>
</cutscene>
<cutsceneid="talkyellow_9"explanation="">
<dialoguespeaker="yellow"english="Vermilion says that he was trapped in some sort of tunnel?"translation="En Vermelló diu que estava atrapat en una mena de túnel?"/>
<dialoguespeaker="player"english="Yeah, it just seemed to keep going and going..."translation="Sí, sembla que es repetia una vegada rere l’altra..."/>
<dialoguespeaker="yellow"english="Interesting... I wonder why it was built?"translation="Interessant... Per què el devien construir?"/>
</cutscene>
<cutsceneid="talkyellow_10"explanation="">
<dialoguespeaker="yellow"english="It's good to be back!"translation="M’alegro de tornar a ser aquí!"/>
<dialoguespeaker="yellow"english="I've got so much work to catch up on..."translation="Tinc molta feina pendent..."/>
</cutscene>
<cutsceneid="talkyellow_11"explanation="">
<dialoguespeaker="yellow"english="I know it's probably a little dangerous to stay here now that this dimension is collapsing..."translation="Suposo que és una mica perillós estar-nos aquí ara que la dimensió s’està esfondrant..."/>
<dialoguespeaker="yellow"english="...but it's so rare to find somewhere this interesting!"translation="Però és molt poc habitual trobar|un lloc tan interessant!"/>
<dialoguespeaker="yellow"english="Maybe we'll find the answers to our own problems here?"translation="Potser hi trobarem les respostes als nostres propis problemes?"/>
</cutscene>
<cutsceneid="talkyellow_trinket1"explanation="">
<dialoguespeaker="yellow"english="Captain! I've been meaning to give this to you..."translation="Cap! Feia temps que et volia donar això..."/>
<dialoguespeaker="player"english="Professor! Where did you find this?"translation="Professor! On ho has trobat?"/>
<dialoguespeaker="yellow"english="Oh, it was just lying around that space station."translation="Ah, era en aquella estació espacial."/>
<dialoguespeaker="yellow"english="It's a pity Doctor Victoria isn't here, she loves studying that sort of thing..."translation="És una llàstima que no hi hagi la doctora Victòria. Li encanta estudiar aquesta mena de coses..."/>
<dialoguespeaker="player"english="Any idea what it does?"translation="Tens cap idea de per a què serveixen?"/>
<dialoguespeaker="yellow"english="Nope! But it is giving off a strange energy reading..."translation="No! Però tenen unes lectures d’energia estranyes..."/>
</cutscene>
<cutsceneid="talkyellow_trinket2"explanation="">
<dialoguespeaker="yellow"english="...so I used the ship's scanner to find more of them!"translation="...així que he fet servir l’escàner de la nau per a trobar-ne més!"/>
<dialoguespeaker="yellow"english="...Please don't let them distract you from finding Victoria, though!"translation="Però... que no et distreguin de trobar la Victòria, d’acord?"/>
<dialoguespeaker="yellow"english="I hope she's ok..."translation="Espero que estigui bé..."/>
</cutscene>
<cutsceneid="talkyellow_trinket3"explanation="">
<dialoguespeaker="yellow"english="Can't seem to detect any more of them nearby, though."translation="Sembla que no en detecto|cap més a prop."/>
<dialoguespeaker="yellow"english="Maybe you've found them all?"translation="Potser ja les has trobades totes?"/>
<dialoguespeaker="cyan"english="Aha! This must be what's causing the interference!"translation="Ahà! Això deu ser el que causa les interferències!"/>
<dialoguespeaker="cyan"english="I wonder if I can turn it off?"translation="Potser el podria apagar?"/>
<dialoguespeaker="gray"english="WARNING: Disabling the Dimensional Stability Generator may lead to instability! Are you sure you want to do this?"translation="ADVERTIMENT: Si desactiveu el generador d’estabilitat dimensional, podeu generar inestabilitat!|Segur que voleu fer això?"/>
<dialoguespeaker="blue"english="I knew you'd be ok!"translation="Sabia que te’n sortiries!"/>
<dialoguespeaker="purple"english="We were very worried when you didn't come back..."translation="Patíem molt per si no tornaves..."/>
<dialoguespeaker="green"english="...but when you turned off the source of the interference..."translation="...però quan has desactivat l’origen de les interferències..."/>
<dialoguespeaker="yellow"english="...we were able to find you with the ship's scanners..."translation="...hem pogut localitzar-te amb els escàners de la nau..."/>
<dialoguespeaker="red"english="...and teleport you back on board!"translation="...i teletransportar-te de nou a bord!"/>
<dialoguespeaker="player"english="That was lucky!"translation="Quina sort!"/>
<dialoguespeaker="yellow"english="...it looks like this dimension is starting to destabilise, just like our own..."translation="...sembla que aquesta dimensió s’està començant a desestabilitzar, igual que la nostra..."/>
<dialoguespeaker="red"english="...we can stay and explore for a little longer, but..."translation="Ens hi podem estar i explorar una mica més, però..."/>
<dialoguespeaker="yellow"english="...eventually, it'll collapse completely."translation="...tard o d’hora, s’esfondrarà per complet."/>
<dialoguespeaker="green"english="There's no telling exactly how long we have here. But the ship's fixed, so..."translation="No sabem exactament quant de temps tenim. Però la nau està reparada, així que..."/>
<dialoguespeaker="blue"english="...as soon as we're ready, we can go home!"translation="...tan bon punt estiguem llestos,|podrem tornar a casa!"/>
<dialoguespeaker="purple"english="What now, Captain?"translation="Què hi ha, cap?"/>
<dialoguespeaker="player"english="Let's find a way to save this dimension!"translation="Trobem una manera de salvar aquesta dimensió!"/>
<dialoguespeaker="player"english="And a way to save our home dimension too!"translation="I una manera de salvar la nostra, alhora!"/>
<dialoguespeaker="player"english="The answer is out there, somewhere!"translation="Segur que la resposta és allà fora, en algun lloc!"/>
<dialoguespeaker="blue"english="I'll run some tests and see if I can work out what they're for..."translation="Faré alguns experiments i miraré de descobrir per a què serveixen..."/>
<dialoguespeaker="player"english="That... that didn't sound good..."translation="Això... no ha sonat gaire bé..."/>
<dialoguespeaker="purple"english="This is where we were storing those shiny things? What happened?"translation="Aquí és on desàvem les coses brillants aquelles, oi? Què ha passat?"/>
<dialoguespeaker="player"english="We were just playing with them, and..."translation="Hi estàvem jugant, i..."/>
<dialoguespeaker="player"english="...they suddenly exploded!"translation="...han explotat de sobte!"/>
<dialoguespeaker="blue"english="But look what they made! Is that a teleporter?"translation="Però mireu què han fet! És un teletransportador?"/>
<dialoguespeaker="yellow"english="I think so, but..."translation="Això sembla, però..."/>
<dialoguespeaker="yellow"english="I've never seen a teleporter like that before..."translation="No he vist mai un teletransportador així..."/>
<dialoguespeaker="red"english="We should investigate!"translation="Ho hauríem d’investigar!"/>
<dialoguespeaker="purple"english="What do you think, Captain?"translation="Què en penses, cap?"/>
<dialoguespeaker="purple"english="Should we find out where it leads?"translation="Hauríem d’investigar on porta?"/>
<dialoguespeaker="purple"english="Or, you know... we could have just warped back to the ship..."translation="O bé... també hauríem pogut teletransportar-nos a la nau..."/>
<dialoguespeaker="green"english="Wow! What is this?"translation="Ostres! Què és això?"/>
<dialoguespeaker="yellow"english="It looks like another laboratory!"translation="Sembla un altre laboratori!"/>
<dialoguespeaker="red"english="Let's have a look around!"translation="Donem-hi un cop d’ull!"/>
</cutscene>
<cutsceneid="talkpurple_9"explanation="">
<dialoguespeaker="purple"english="Look at all this research! This is going to be a big help back home!"translation="Mira quanta recerca! Això ens serà de gran ajuda quan tornem a casa!"/>
</cutscene>
<cutsceneid="talkgreen_11"explanation="">
<dialoguespeaker="green"english="I wonder why they abandoned this dimension? They were so close to working out how to fix it..."translation="Per què devien abandonar aquesta dimensió? Eren molt a prop de descobrir com reparar-la..."/>
<dialoguespeaker="green"english="Maybe we can fix it for them? Maybe they'll come back?"translation="Potser la podem reparar en nom seu? Potser tornaran?"/>
</cutscene>
<cutsceneid="talkblue_9"explanation="">
<dialoguespeaker="blue"english="This lab is amazing! The scientists who worked here know a lot more about warp technology than we do!"translation="Aquest laboratori és increïble! Els científics que hi treballaven sabien moltes més coses de la tecnologia del teletransport que no pas nosaltres!"/>
</cutscene>
<cutsceneid="talkyellow_12"explanation="">
<dialoguespeaker="yellow"english="Captain! Have you seen this?"translation="Cap! Has vist això?"/>
<dialoguespeaker="yellow"english="With their research and ours, we should be able to stabilise our own dimension!"translation="Si ajuntem les seves investigacions i les nostres, potser podríem estabilitzar la nostra dimensió!"/>
<dialoguespeaker="red"english="Look what I found!"translation="Mira què he trobat!"/>
<dialoguespeaker="red"english="It's pretty hard, I can only last for about 10 seconds..."translation="És força difícil, només hi he durat uns deu segons..."/>
</cutscene>
<cutsceneid="terminal_jukebox"explanation="">
<dialoguespeaker="gray"english="-= JUKEBOX =-
Songs will continue to play until you leave the ship.
Collect trinkets to unlock new songs!"translation="-= TOCADISCOS =-
Les cançons continuaran sonant fins que surtis de la nau.
Recull lluentons per a desblocar cançons noves!"centertext="1"padtowidth="264"/>
</cutscene>
<cutsceneid="terminal_jukeunlock1"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
5 Trinkets
Pushing Onwards"translation="SEGÜENT PISTA A DESBLOCAR:
5 lluentons
Prement sempre endavant"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock2"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
8 Trinkets
Positive Force"translation="SEGÜENT PISTA A DESBLOCAR:
8 lluentons
Positivitat en la força"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock3"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
10 Trinkets
Presenting VVVVVV"translation="SEGÜENT PISTA A DESBLOCAR:
10 lluentons
Presentació de VVVVVV"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock4"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
12 Trinkets
Potential for Anything"translation="SEGÜENT PISTA A DESBLOCAR:
Pressure Cooker"translation="SEGÜENT PISTA A DESBLOCAR:
14 lluentons
Pressió dins l’olla"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock5"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
16 Trinkets
Predestined Fate"translation="SEGÜENT PISTA A DESBLOCAR:
16 lluentons
Predestinació atzarosa"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock6"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
18 Trinkets
Popular Potpourri"translation="SEGÜENT PISTA A DESBLOCAR:
18 lluentons
Popurri popular"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_jukeunlock7"explanation="">
<dialoguespeaker="gray"english="NEXT UNLOCK:
20 Trinkets
Pipe Dream"translation="SEGÜENT PISTA A DESBLOCAR:
20 lluentons
Pretensió impossible"tt="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_station_1"explanation="">
<dialoguespeaker="gray"english="-= PERSONAL LOG =-"translation="-= DIARI PERSONAL =-"padtowidth="280"/>
<dialoguespeaker="gray"english="Almost everyone has been evacuated from the space station now. The rest of us are leaving in a couple of days, once our research has been completed."translation="Ja han evacuat gairebé tothom de l’estació espacial. La resta|ens n’anirem d’aquí a un parell de dies, quan hàgim acabat la recerca."pad="1"/>
</cutscene>
<cutsceneid="terminal_station_2"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...everything collapses, eventually. It's the way of the universe."translation="...tot s’acaba esfondrant, tard o d’hora. L’univers és així."centertext="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_station_3"explanation="">
<dialoguespeaker="gray"english="I wonder if the generator we set up in the polar dimension is what's affecting our teleporters?"translation="Potser el que afecta els nostres|teletransportadors és el generador|que vam instal·lar a la dimensió polar?"/>
<dialoguespeaker="gray"english="No, it's probably just a glitch."translation="No, segurament només és un error."/>
</cutscene>
<cutsceneid="terminal_station_4"explanation="a trinket that's difficult to get">
<dialoguespeaker="gray"english="-= PERSONAL LOG =-"translation="-= DIARI PERSONAL =-"padtowidth="280"/>
<dialoguespeaker="gray"english="Hah! Nobody will ever get this one."translation="Ha! Aquest no l’agafarà mai ningú."pad="1"/>
</cutscene>
<cutsceneid="terminal_warp_1"explanation="">
<dialoguespeaker="gray"english="...The other day I was chased down a hallway by a giant cube with the word AVOID on it."translation="L’altre dia un cub gegant amb la paraula EVITA em va perseguir per una sala."/>
<dialoguespeaker="gray"english="These security measures go too far!"translation="Aquestes mesures de seguretat són massa estrictes!"/>
</cutscene>
<cutsceneid="terminal_warp_2"explanation="">
<dialoguespeaker="gray"english="The only way into my private lab anymore is by teleporter."translation="L’única manera d’entrar al meu laboratori privat és amb un teletransportador."/>
<dialoguespeaker="gray"english="I've made sure that it's difficult for unauthorised personnel to gain access."translation="M’he assegurat que sigui difícil que personal no autoritzat hi obtingui accés."/>
</cutscene>
<cutsceneid="terminal_outside_1"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... our first breakthrough was the creation of the inversion plane, which creates a mirrored dimension beyond a given event horizon ..."translation="...el nostre primer avenç va ser la creació del pla d’inversió, que crea una dimensió mirall més enllà d’un horitzó d’esdeveniments|concret..."pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_2"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...with just a small modification to the usual parameters, we were able to stabilise an infinite tunnel!"translation="...sols amb una petita modificació dels paràmetres normals, vam poder estabilitzar un túnel infinit!"/>
</cutscene>
<cutsceneid="terminal_outside_3"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... the final step in creating the dimensional stabiliser was to create a feedback loop ..."translation="...l’últim pas a l’hora de crear l’estabilitzador dimensional va ser crear un bucle de retroalimentació..."pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_4"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="...despite our best efforts, the dimensional stabiliser won't hold out forever. Its collapse is inevitable..."translation="...malgrat els nostres esforços, l’estabilitzador dimensional no funcionarà eternament. En algun moment, s’esfondrarà..."pad="1"/>
<dialoguespeaker="cyan"english="Huh? These coordinates aren't even in this dimension!"translation="Eh? Aquestes coordenades no són ni tan sols en aquesta dimensió!"/>
</cutscene>
<cutsceneid="terminal_outside_5"explanation="">
<dialoguespeaker="gray"english="-= Personal Log =-"translation="-= DIARI PERSONAL =-"padtowidth="232"/>
<dialoguespeaker="gray"english="... I've had to seal off access to most of our research. Who knows what could happen if it fell into the wrong hands? ..."translation="...he hagut de segellar l’accés a la major part de la recerca. Qui sap què podria passar si caigués en les mans equivocades..."centertext="1"pad="1"/>
</cutscene>
<cutsceneid="terminal_outside_6"explanation="">
<dialoguespeaker="gray"english="-= Research Notes =-"translation="-= NOTES DE RECERCA =-"padtowidth="264"/>
<dialoguespeaker="gray"english="... access to the control center is still possible through the main atmospheric filters ..."translation="...l’accés al centre de control encara és possible a través dels filtres atmosfèrics principals..."/>
</cutscene>
<cutsceneid="terminal_lab_1"explanation="">
<dialoguespeaker="gray"english="... it turns out the key to stabilising this dimension was to create a balancing force outside of it!"translation="...resulta que la clau per a estabilitzar aquesta dimensió era crear una força equilibradora fora de la dimensió!"/>
<dialoguespeaker="gray"english="Though it looks like that's just a temporary solution, at best."translation="Tot i que això sembla només una solució temporal, com a molt."/>
<dialoguespeaker="gray"english="I've been working on something more permanent, but it seems it's going to be too late..."translation="He estat treballant en una cosa més permanent, però sembla que ja serà massa tard..."/>
</cutscene>
<cutsceneid="terminal_lab_2"explanation="">
<dialoguespeaker="gray"english="?SYNTAX ERROR"translation="?ERROR DE SINTAXI"/>
</cutscene>
<cutsceneid="terminal_letsgo"explanation="">
<dialoguespeaker="player"english="Now that the ship is fixed, we can leave anytime we want!"translation="Ara que la nau ja està reparada, ens en podem anar quan vulguem!"/>
<dialoguespeaker="player"english="We've all agreed to keep exploring this dimension, though."translation="Però tots hem estat d’acord a continuar explorant aquesta dimensió."/>
<dialoguespeaker="player"english="Who knows what we'll find?"translation="Qui sap què hi trobarem!"/>
</cutscene>
<cutsceneid="terminal_radio"explanation="">
<dialoguespeaker="gray"english="-= SHIP RADIO =-
[ Status ]
Broadcasting"translation="-= RÀDIO DE LA NAU =-
[ Estat ]
S’està transmetent"tt="1"centertext="1"pad="2"/>
</cutscene>
<cutsceneid="terminal_secretlab"explanation="">
<dialoguespeaker="gray"english="-= WARNING =-
The Super-Gravitron is intended for entertainment purposes only."translation="-= ADVERTIMENT =-
El supergravitró només té finalitats d’entreteniment."centertext="1"pad="1"/>
<dialoguespeaker="gray"english="Anyone found using the Super Gravitron for educational purposes may be asked to stand in the naughty corner."translation="Si algú el fa servir per a finalitats educatives, és|possible que l’enviem al racó de pensar."/>
Controls de navegació de la nau"centertext="1"pad="1"/>
<dialoguespeaker="gray"english="Error! Error! Cannot isolate dimensional coordinates! Interference detected!"translation="Error! Error! No és possible aïllar les coordenades dimensionals! S’han detectat interferències!"/>
</cutscene>
<cutsceneid="alreadyvisited"explanation="">
<dialoguespeaker="cyan"english="...oh, I've already found this."translation="Oh, això ja ho havia trobat..."/>
</cutscene>
<cutsceneid="disableaccessibility"explanation="">
<dialoguespeaker="gray"english="Please disable invincibility and/or slowdown before entering the Super Gravitron."translation="Desactiva la invencibilitat i/o l’alentiment abans d’entrar al supergravitró."/>
<!-- Enable for RTL languages like Arabic or Hebrew -->
<rtl>0</rtl>
<!-- The indication that a certain menu option or button is selected -->
<menu_select>[ {label} ]</menu_select>
<menu_select_tight>[{label}]</menu_select_tight>
<!-- The filename of the font to use. For example, "font_cn" means font_cn.png and font_cn.fontmeta. -->
<font>font</font>
</langmeta>
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.