2020-09-28 04:15:06 +02:00
|
|
|
#define GAME_DEFINITION
|
2020-01-01 21:29:24 +01:00
|
|
|
#include "Game.h"
|
|
|
|
|
2020-07-19 21:43:29 +02:00
|
|
|
#include <sstream>
|
2020-01-01 21:29:24 +01:00
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
2020-07-19 21:03:16 +02:00
|
|
|
#include <tinyxml2.h>
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Add support for button glyph display
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).
2023-03-18 22:30:16 +01:00
|
|
|
#include "ButtonGlyphs.h"
|
2021-09-13 06:39:07 +02:00
|
|
|
#include "Constants.h"
|
2021-02-20 08:19:09 +01:00
|
|
|
#include "CustomLevels.h"
|
Remove game.shouldreturntoeditor in favor of using defer callback
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.
2021-01-10 23:48:25 +01:00
|
|
|
#include "DeferCallbacks.h"
|
2021-02-21 00:40:11 +01:00
|
|
|
#include "Editor.h"
|
2020-07-19 21:43:29 +02:00
|
|
|
#include "Entity.h"
|
|
|
|
#include "Enums.h"
|
|
|
|
#include "FileSystemUtils.h"
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
#include "GlitchrunnerMode.h"
|
2020-07-19 21:43:29 +02:00
|
|
|
#include "Graphics.h"
|
2023-07-02 04:02:25 +02:00
|
|
|
#include "LevelDebugger.h"
|
2022-12-30 22:57:24 +01:00
|
|
|
#include "Localization.h"
|
|
|
|
#include "LocalizationStorage.h"
|
2020-11-13 02:16:18 +01:00
|
|
|
#include "KeyPoll.h"
|
2020-01-01 21:29:24 +01:00
|
|
|
#include "MakeAndPlay.h"
|
2020-07-19 21:43:29 +02:00
|
|
|
#include "Map.h"
|
|
|
|
#include "Music.h"
|
|
|
|
#include "Network.h"
|
2022-12-30 22:57:24 +01:00
|
|
|
#include "RoomnameTranslator.h"
|
Extern `gameScreen`, remove `screenbuffer`
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.
2021-12-25 08:56:47 +01:00
|
|
|
#include "Screen.h"
|
2020-07-19 21:43:29 +02:00
|
|
|
#include "Script.h"
|
2023-02-04 09:14:04 +01:00
|
|
|
#include "Unused.h"
|
2024-01-05 02:04:27 +01:00
|
|
|
#include "UTF8.h"
|
2020-07-19 21:43:29 +02:00
|
|
|
#include "UtilityClass.h"
|
2022-12-30 22:57:24 +01:00
|
|
|
#include "VFormat.h"
|
2021-02-24 00:21:29 +01:00
|
|
|
#include "Vlogging.h"
|
2020-09-25 18:09:21 +02:00
|
|
|
#include "XMLUtils.h"
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-01-10 18:14:37 +01:00
|
|
|
static bool GetButtonFromString(const char *pText, SDL_GameControllerButton *button)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 22:01:55 +02:00
|
|
|
if (*pText == '0' ||
|
|
|
|
*pText == 'a' ||
|
|
|
|
*pText == 'A')
|
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_A;
|
|
|
|
return true;
|
|
|
|
}
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
if (SDL_strcmp(pText, "1") == 0 ||
|
2020-04-02 22:01:55 +02:00
|
|
|
*pText == 'b' ||
|
|
|
|
*pText == 'B')
|
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_B;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '2' ||
|
|
|
|
*pText == 'x' ||
|
|
|
|
*pText == 'X')
|
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_X;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '3' ||
|
|
|
|
*pText == 'y' ||
|
|
|
|
*pText == 'Y')
|
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_Y;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '4' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "BACK") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_BACK;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '5' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "GUIDE") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_GUIDE;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '6' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "START") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_START;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '7' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "LS") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_LEFTSTICK;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '8' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "RS") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (*pText == '9' ||
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
SDL_strcasecmp(pText, "LB") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
|
|
|
return true;
|
|
|
|
}
|
Reduce dependency on libc functions
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)
2021-01-12 01:17:45 +01:00
|
|
|
if (SDL_strcmp(pText, "10") == 0 ||
|
|
|
|
SDL_strcasecmp(pText, "RB") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
*button = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
// Unfortunate forward-declare... My hands are pretty tied
|
|
|
|
static void loadthissummary(
|
|
|
|
const char* filename,
|
|
|
|
struct Game::Summary* summary,
|
|
|
|
tinyxml2::XMLDocument& doc
|
|
|
|
);
|
|
|
|
|
|
|
|
static struct Game::Summary get_summary(
|
2021-03-23 22:42:33 +01:00
|
|
|
const char* filename,
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
const char* savename,
|
2021-03-23 22:42:33 +01:00
|
|
|
tinyxml2::XMLDocument& doc
|
|
|
|
) {
|
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
struct Game::Summary summary;
|
|
|
|
SDL_zero(summary);
|
2021-03-23 22:42:33 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (!FILESYSTEM_loadTiXml2Document(filename, doc))
|
2021-03-23 22:42:33 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_info("%s not found", savename);
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
return summary;
|
2021-03-23 22:42:33 +01:00
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
loadthissummary(savename, &summary, doc);
|
2021-03-23 22:42:33 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
return summary;
|
2021-03-23 22:42:33 +01:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Allow using help/graphics/music/game/key/map/obj everywhere
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.
2020-01-29 08:35:03 +01:00
|
|
|
void Game::init(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-02-21 21:09:43 +01:00
|
|
|
SDL_strlcpy(magic, "[vVvVvV]game", sizeof(magic));
|
|
|
|
|
2020-06-25 23:49:54 +02:00
|
|
|
roomx = 0;
|
|
|
|
roomy = 0;
|
|
|
|
prevroomx = 0;
|
|
|
|
prevroomy = 0;
|
|
|
|
saverx = 0;
|
|
|
|
savery = 0;
|
2021-09-07 03:28:28 +02:00
|
|
|
savecolour = 0;
|
2020-06-25 23:49:54 +02:00
|
|
|
|
Allow using help/graphics/music/game/key/map/obj everywhere
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.
2020-01-29 08:35:03 +01:00
|
|
|
mutebutton = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
muted = false;
|
2020-04-19 21:40:59 +02:00
|
|
|
musicmuted = false;
|
|
|
|
musicmutebutton = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-04-02 22:01:55 +02:00
|
|
|
glitchrunkludge = false;
|
Fix bringing up map menu during gamemode(teleporter)
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.
2020-12-29 00:36:32 +01:00
|
|
|
gamestate = TITLEMODE;
|
|
|
|
prevgamestate = TITLEMODE;
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
jumpheld = false;
|
|
|
|
advancetext = false;
|
|
|
|
jumppressed = 0;
|
|
|
|
gravitycontrol = 0;
|
|
|
|
teleport = false;
|
|
|
|
edteleportent = 0; //Added in the port!
|
|
|
|
companion = 0;
|
|
|
|
|
|
|
|
|
|
|
|
quickrestartkludge = false;
|
|
|
|
|
|
|
|
tapleft = 0;
|
|
|
|
tapright = 0;
|
|
|
|
|
2021-04-19 08:23:44 +02:00
|
|
|
press_right = false;
|
|
|
|
press_left = false;
|
|
|
|
press_action = false;
|
|
|
|
press_map = false;
|
|
|
|
press_interact = false;
|
|
|
|
interactheld = false;
|
|
|
|
separate_interact = false;
|
|
|
|
mapheld = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
pausescript = false;
|
|
|
|
completestop = false;
|
|
|
|
activeactivity = -1;
|
|
|
|
act_fade = 0;
|
2020-04-29 06:58:19 +02:00
|
|
|
prev_act_fade = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
backgroundtext = false;
|
|
|
|
startscript = false;
|
|
|
|
inintermission = false;
|
|
|
|
|
|
|
|
alarmon = false;
|
|
|
|
alarmdelay = 0;
|
|
|
|
blackout = false;
|
|
|
|
creditposx = 0;
|
|
|
|
creditposy = 0;
|
|
|
|
creditposdelay = 0;
|
2020-05-02 03:23:52 +02:00
|
|
|
oldcreditposx = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
useteleporter = false;
|
|
|
|
teleport_to_teleporter = 0;
|
|
|
|
|
|
|
|
activetele = false;
|
|
|
|
readytotele = 0;
|
2020-04-30 02:04:25 +02:00
|
|
|
oldreadytotele = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
activity_r = 0;
|
|
|
|
activity_g = 0;
|
|
|
|
activity_b = 0;
|
Add `setactivityposition(x,y)`, add new textbox color `transparent` (#847)
* 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.
2021-10-14 00:38:51 +02:00
|
|
|
activity_y = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
creditposition = 0;
|
2020-04-30 19:56:27 +02:00
|
|
|
oldcreditposition = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
bestgamedeaths = -1;
|
|
|
|
|
|
|
|
//Accessibility Options
|
|
|
|
colourblindmode = false;
|
|
|
|
noflashingmode = false;
|
|
|
|
slowdown = 30;
|
|
|
|
|
|
|
|
nodeathmode = false;
|
|
|
|
nocutscenes = false;
|
2021-01-08 01:18:07 +01:00
|
|
|
ndmresultcrewrescued = 0;
|
|
|
|
ndmresulttrinkets = 0;
|
2024-01-22 03:32:18 +01:00
|
|
|
ndmresulthardestroom.clear();
|
|
|
|
ndmresulthardestroom_x = hardestroom_x;
|
|
|
|
ndmresulthardestroom_y = hardestroom_y;
|
|
|
|
ndmresulthardestroom_specialname = false;
|
2024-08-30 19:19:27 +02:00
|
|
|
nodeatheligible = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
customcol=0;
|
|
|
|
|
2020-07-03 03:10:52 +02:00
|
|
|
SDL_memset(crewstats, false, sizeof(crewstats));
|
2021-01-08 01:18:07 +01:00
|
|
|
SDL_memset(ndmresultcrewstats, false, sizeof(ndmresultcrewstats));
|
2020-07-03 02:09:30 +02:00
|
|
|
SDL_memset(besttimes, -1, sizeof(besttimes));
|
2020-07-01 05:02:18 +02:00
|
|
|
SDL_memset(bestframes, -1, sizeof(bestframes));
|
2020-07-03 02:09:30 +02:00
|
|
|
SDL_memset(besttrinkets, -1, sizeof(besttrinkets));
|
|
|
|
SDL_memset(bestlives, -1, sizeof(bestlives));
|
|
|
|
SDL_memset(bestrank, -1, sizeof(bestrank));
|
2020-01-12 12:28:34 +01:00
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
crewstats[0] = true;
|
|
|
|
lastsaved = 0;
|
|
|
|
|
|
|
|
//Menu stuff initiliased here:
|
2020-07-03 01:45:22 +02:00
|
|
|
SDL_memset(unlock, false, sizeof(unlock));
|
|
|
|
SDL_memset(unlocknotify, false, sizeof(unlock));
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
currentmenuoption = 0;
|
2022-12-30 22:57:24 +01:00
|
|
|
menutestmode = false;
|
2020-02-12 05:45:58 +01:00
|
|
|
current_credits_list_index = 0;
|
2023-12-06 00:34:05 +01:00
|
|
|
translator_credits_pagenum = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
menuxoff = 0;
|
|
|
|
menuyoff = 0;
|
|
|
|
menucountdown = 0;
|
|
|
|
levelpage=0;
|
|
|
|
playcustomlevel=0;
|
|
|
|
|
2020-11-22 03:10:26 +01:00
|
|
|
silence_settings_error = false;
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
deathcounts = 0;
|
|
|
|
gameoverdelay = 0;
|
2022-11-14 23:10:24 +01:00
|
|
|
framecounter = 0;
|
|
|
|
seed_use_sdl_getticks = false;
|
2023-08-23 19:51:11 +02:00
|
|
|
editor_disabled = false;
|
2021-04-13 23:36:21 +02:00
|
|
|
resetgameclock();
|
2020-01-01 21:29:24 +01:00
|
|
|
gamesaved = false;
|
2020-11-04 03:45:33 +01:00
|
|
|
gamesavefailed = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
savetime = "00:00";
|
|
|
|
savetrinkets = 0;
|
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
/* These are only used some of the time. */
|
|
|
|
saveframes = 0;
|
|
|
|
saveseconds = 0;
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
intimetrial = false;
|
|
|
|
timetrialcountdown = 0;
|
|
|
|
timetrialshinytarget = 0;
|
|
|
|
timetrialparlost = false;
|
|
|
|
timetrialpar = 0;
|
2022-12-30 22:57:24 +01:00
|
|
|
timetrialcheater = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
timetrialresulttime = 0;
|
2020-06-30 00:53:19 +02:00
|
|
|
timetrialresultframes = 0;
|
2021-01-08 01:02:45 +01:00
|
|
|
timetrialresultshinytarget = 0;
|
|
|
|
timetrialresulttrinkets = 0;
|
|
|
|
timetrialresultpar = 0;
|
|
|
|
timetrialresultdeaths = 0;
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
start_translator_exploring = false;
|
|
|
|
translator_exploring = false;
|
|
|
|
translator_exploring_allowtele = false;
|
2022-12-24 04:16:56 +01:00
|
|
|
translator_cutscene_test = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
totalflips = 0;
|
|
|
|
hardestroom = "Welcome Aboard";
|
|
|
|
hardestroomdeaths = 0;
|
2023-09-04 15:27:29 +02:00
|
|
|
hardestroom_x = 13;
|
|
|
|
hardestroom_y = 5;
|
|
|
|
hardestroom_specialname = false;
|
|
|
|
hardestroom_finalstretch = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
currentroomdeaths=0;
|
|
|
|
|
|
|
|
inertia = 1.1f;
|
|
|
|
swnmode = false;
|
|
|
|
swntimer = 0;
|
2023-06-05 08:24:31 +02:00
|
|
|
swngame = SWN_NONE; // Not playing sine wave ninja!
|
2020-01-01 21:29:24 +01:00
|
|
|
swnstate = 0;
|
|
|
|
swnstate2 = 0;
|
|
|
|
swnstate3 = 0;
|
|
|
|
swnstate4 = 0;
|
|
|
|
swndelay = 0;
|
|
|
|
swndeaths = 0;
|
|
|
|
supercrewmate = false;
|
|
|
|
scmhurt = false;
|
|
|
|
scmprogress = 0;
|
|
|
|
swncolstate = 0;
|
|
|
|
swncoldelay = 0;
|
|
|
|
swnrecord = 0;
|
|
|
|
swnbestrank = 0;
|
|
|
|
swnrank = 0;
|
|
|
|
swnmessage = 0;
|
|
|
|
|
|
|
|
clearcustomlevelstats();
|
|
|
|
|
|
|
|
saveFilePath = FILESYSTEM_getUserSaveDirectory();
|
|
|
|
|
2020-06-04 04:29:44 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
last_quicksave = get_summary("saves/qsave.vvv", "qsave.vvv", doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-06-04 04:29:44 +02:00
|
|
|
tinyxml2::XMLDocument docTele;
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
last_telesave = get_summary("saves/tsave.vvv", "tsave.vvv", doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
screenshake = flashlight = 0 ;
|
|
|
|
|
|
|
|
stat_trinkets = 0;
|
|
|
|
|
|
|
|
state = 1;
|
2022-12-08 02:34:11 +01:00
|
|
|
statedelay = 0;
|
2022-12-07 00:20:48 +01:00
|
|
|
statelocked = false;
|
2020-03-31 02:16:02 +02:00
|
|
|
//updatestate();
|
2020-01-13 02:45:44 +01:00
|
|
|
|
|
|
|
skipfakeload = false;
|
2020-01-17 02:14:56 +01:00
|
|
|
|
Add a player trail to the editor (ghosts)
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)
2020-06-13 00:04:35 +02:00
|
|
|
ghostsenabled = false;
|
|
|
|
|
2020-04-09 21:03:24 +02:00
|
|
|
cliplaytest = false;
|
|
|
|
playx = 0;
|
|
|
|
playy = 0;
|
|
|
|
playrx = 0;
|
|
|
|
playry = 0;
|
|
|
|
playgc = 0;
|
|
|
|
|
2020-05-08 00:23:55 +02:00
|
|
|
fadetomenu = false;
|
|
|
|
fadetomenudelay = 0;
|
2020-05-08 00:30:26 +02:00
|
|
|
fadetolab = false;
|
|
|
|
fadetolabdelay = 0;
|
2020-05-08 00:23:55 +02:00
|
|
|
|
2021-04-09 12:10:22 +02:00
|
|
|
over30mode = true;
|
2021-08-05 23:31:20 +02:00
|
|
|
showingametimer = false;
|
2020-06-23 01:51:16 +02:00
|
|
|
|
|
|
|
ingame_titlemode = false;
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
ingame_editormode = false;
|
2023-08-23 19:51:11 +02:00
|
|
|
|
2020-06-23 02:23:56 +02:00
|
|
|
kludge_ingametemp = Menu::mainmenu;
|
2021-04-12 02:43:17 +02:00
|
|
|
slidermode = SLIDER_NONE;
|
2020-05-04 21:52:57 +02:00
|
|
|
|
2020-06-30 04:49:14 +02:00
|
|
|
disablepause = false;
|
2021-08-05 21:20:05 +02:00
|
|
|
disableaudiopause = false;
|
2021-08-12 03:08:32 +02:00
|
|
|
disabletemporaryaudiopause = true;
|
2021-04-02 00:39:56 +02:00
|
|
|
inputdelay = false;
|
Fix regression with controller binds on first launch
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 3ef5248db93541f399e8220fe0280281881d8bd6 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.
2023-04-15 20:14:35 +02:00
|
|
|
|
2023-05-22 21:18:40 +02:00
|
|
|
old_skip_message_timer = 0;
|
|
|
|
skip_message_timer = 0;
|
|
|
|
|
2024-01-07 05:53:51 +01:00
|
|
|
old_mode_indicator_timer = 0;
|
|
|
|
mode_indicator_timer = 0;
|
|
|
|
|
2024-01-09 19:55:00 +01:00
|
|
|
old_screenshot_border_timer = 0;
|
|
|
|
screenshot_border_timer = 0;
|
|
|
|
screenshot_saved_success = false;
|
|
|
|
|
Fix regression with controller binds on first launch
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 3ef5248db93541f399e8220fe0280281881d8bd6 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.
2023-04-15 20:14:35 +02:00
|
|
|
setdefaultcontrollerbuttons();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::setdefaultcontrollerbuttons(void)
|
|
|
|
{
|
|
|
|
if (controllerButton_flip.size() < 1)
|
|
|
|
{
|
|
|
|
controllerButton_flip.push_back(SDL_CONTROLLER_BUTTON_A);
|
|
|
|
}
|
|
|
|
if (controllerButton_map.size() < 1)
|
|
|
|
{
|
|
|
|
controllerButton_map.push_back(SDL_CONTROLLER_BUTTON_Y);
|
|
|
|
}
|
|
|
|
if (controllerButton_esc.size() < 1)
|
|
|
|
{
|
|
|
|
controllerButton_esc.push_back(SDL_CONTROLLER_BUTTON_B);
|
|
|
|
}
|
|
|
|
if (controllerButton_restart.size() < 1)
|
|
|
|
{
|
|
|
|
controllerButton_restart.push_back(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
|
|
|
|
}
|
|
|
|
if (controllerButton_interact.size() < 1)
|
|
|
|
{
|
|
|
|
controllerButton_interact.push_back(SDL_CONTROLLER_BUTTON_X);
|
|
|
|
}
|
Fix binding menu allowing you to lock yourself out of the game
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.
2023-12-09 22:15:21 +01:00
|
|
|
|
|
|
|
/* If one of the arrays was empty, and others weren't, we might now have conflicts...
|
|
|
|
* A crucial one is if the ACTION button is also "ESC", because then you can't
|
|
|
|
* fix it with just a controller anymore, which might make the game unplayable.
|
|
|
|
* This is similar to updatebuttonmappings() in Input.cpp, except less... complete? */
|
|
|
|
for (size_t f = 0; f < controllerButton_flip.size(); f++)
|
|
|
|
{
|
|
|
|
for (size_t e = 0; e < controllerButton_esc.size(); e++)
|
|
|
|
{
|
|
|
|
if (controllerButton_flip[f] == controllerButton_esc[e])
|
|
|
|
{
|
|
|
|
controllerButton_esc.erase(controllerButton_esc.begin() + e);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::lifesequence(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
if (lifeseq > 0)
|
|
|
|
{
|
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
if (lifeseq == 2) obj.entities[i].invis = true;
|
|
|
|
if (lifeseq == 6) obj.entities[i].invis = true;
|
|
|
|
if (lifeseq >= 8) obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (lifeseq > 5) gravitycontrol = savegc;
|
|
|
|
|
|
|
|
lifeseq--;
|
2023-04-06 01:45:40 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && (lifeseq <= 0 || noflashingmode))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::clearcustomlevelstats(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
//just clearing the array
|
|
|
|
customlevelstats.clear();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Game::updatecustomlevelstats(std::string clevel, int cscore)
|
|
|
|
{
|
2023-05-19 04:58:23 +02:00
|
|
|
if (!map.custommodeforreal)
|
|
|
|
{
|
|
|
|
/* We are playtesting, don't update level stats */
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
if (clevel.find("levels/") != std::string::npos)
|
|
|
|
{
|
|
|
|
clevel = clevel.substr(7);
|
|
|
|
}
|
2023-10-25 04:05:23 +02:00
|
|
|
if (customlevelstats.count(clevel) == 0 || cscore > customlevelstats[clevel])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
customlevelstats[clevel] = cscore;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
savecustomlevelstats();
|
|
|
|
}
|
|
|
|
|
2021-08-12 05:54:02 +02:00
|
|
|
void Game::deletecustomlevelstats(void)
|
|
|
|
{
|
|
|
|
customlevelstats.clear();
|
|
|
|
|
|
|
|
if (!FILESYSTEM_delete("saves/levelstats.vvv"))
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting levelstats.vvv");
|
2021-08-12 05:54:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-12 21:13:54 +01:00
|
|
|
#define LOAD_ARRAY_RENAME(ARRAY_NAME, DEST) \
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, #ARRAY_NAME) == 0 && pText[0] != '\0') \
|
2021-02-12 21:13:54 +01:00
|
|
|
{ \
|
Refactor loading arrays from XML to not use the STL
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.
2021-02-13 01:37:29 +01:00
|
|
|
/* We're loading in 32-bit integers. If we need more than 16 chars,
|
|
|
|
* something is seriously wrong */ \
|
|
|
|
char buffer[16]; \
|
|
|
|
size_t start = 0; \
|
|
|
|
size_t i = 0; \
|
|
|
|
\
|
|
|
|
while (next_split_s(buffer, sizeof(buffer), &start, pText, ',')) \
|
2021-02-12 22:53:01 +01:00
|
|
|
{ \
|
Refactor loading arrays from XML to not use the STL
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.
2021-02-13 01:37:29 +01:00
|
|
|
if (i >= SDL_arraysize(DEST)) \
|
|
|
|
{ \
|
|
|
|
break; \
|
|
|
|
} \
|
|
|
|
\
|
|
|
|
DEST[i] = help.Int(buffer); \
|
|
|
|
++i; \
|
2021-02-12 22:53:01 +01:00
|
|
|
} \
|
2021-02-12 21:13:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#define LOAD_ARRAY(ARRAY_NAME) LOAD_ARRAY_RENAME(ARRAY_NAME, ARRAY_NAME)
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::loadcustomlevelstats(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-30 03:47:45 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
if (!FILESYSTEM_loadTiXml2Document("saves/levelstats.vvv", doc))
|
|
|
|
{
|
|
|
|
//No levelstats file exists; start new
|
|
|
|
customlevelstats.clear();
|
|
|
|
savecustomlevelstats();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing levelstats.vvv: %s", doc.ErrorStr());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-04 02:21:35 +02:00
|
|
|
customlevelstats.clear();
|
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
tinyxml2::XMLElement* pElem;
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
tinyxml2::XMLElement* firstElement;
|
2020-06-30 03:47:45 +02:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
firstElement = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
// First pass, look for the new system of storing stats
|
|
|
|
// If they don't exist, then fall back to the old system
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = firstElement; pElem != NULL; pElem = pElem->NextSiblingElement())
|
2020-06-30 03:47:45 +02:00
|
|
|
{
|
2021-03-06 19:31:17 +01:00
|
|
|
const char* pKey = pElem->Value();
|
2020-06-30 03:47:45 +02:00
|
|
|
const char* pText = pElem->GetText();
|
|
|
|
if (pText == NULL)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-30 03:47:45 +02:00
|
|
|
pText = "";
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "stats") == 0)
|
2020-06-30 03:47:45 +02:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
bool file_has_duplicates = false;
|
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
for (tinyxml2::XMLElement* stat_el = pElem->FirstChildElement(); stat_el; stat_el = stat_el->NextSiblingElement())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
int score = 0;
|
|
|
|
std::string name;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
if (stat_el->GetText() != NULL)
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
score = help.Int(stat_el->GetText());
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
if (stat_el->Attribute("name"))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
name = stat_el->Attribute("name");
|
|
|
|
}
|
|
|
|
|
|
|
|
int existing = customlevelstats.count(name);
|
|
|
|
if (existing > 0)
|
|
|
|
{
|
|
|
|
file_has_duplicates = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (existing == 0 || score > customlevelstats[name])
|
|
|
|
{
|
|
|
|
customlevelstats[name] = score;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-10-25 04:05:23 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-10-25 04:05:23 +02:00
|
|
|
if (file_has_duplicates)
|
|
|
|
{
|
|
|
|
/* This might be really inflated, so simply save the map we have now,
|
|
|
|
* so we don't have to keep loading a 90 MB file. */
|
|
|
|
savecustomlevelstats();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
// Since we're still here, we must be on the old system
|
2023-10-25 02:57:50 +02:00
|
|
|
std::vector<std::string> customlevelnames;
|
|
|
|
std::vector<int> customlevelscores;
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = firstElement; pElem; pElem=pElem->NextSiblingElement())
|
2020-06-30 03:47:45 +02:00
|
|
|
{
|
2021-03-06 19:31:17 +01:00
|
|
|
const char* pKey = pElem->Value();
|
2020-06-30 03:47:45 +02:00
|
|
|
const char* pText = pElem->GetText() ;
|
|
|
|
if(pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-04-09 17:45:24 +02:00
|
|
|
if (SDL_strcmp(pKey, "customlevelscore") == 0 && pText[0] != '\0')
|
|
|
|
{
|
|
|
|
char buffer[16];
|
|
|
|
size_t start = 0;
|
|
|
|
|
|
|
|
while (next_split_s(buffer, sizeof(buffer), &start, pText, ','))
|
|
|
|
{
|
|
|
|
customlevelscores.push_back(help.Int(buffer));
|
|
|
|
}
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "customlevelstats") == 0 && pText[0] != '\0')
|
2020-06-30 03:47:45 +02:00
|
|
|
{
|
Refactor loading arrays from XML to not use the STL
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.
2021-02-13 01:37:29 +01:00
|
|
|
size_t start = 0;
|
|
|
|
size_t len = 0;
|
|
|
|
size_t prev_start = 0;
|
|
|
|
|
|
|
|
while (next_split(&start, &len, &pText[start], '|'))
|
2021-02-12 22:53:01 +01:00
|
|
|
{
|
Refactor loading arrays from XML to not use the STL
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.
2021-02-13 01:37:29 +01:00
|
|
|
customlevelnames.push_back(std::string(&pText[prev_start], len));
|
|
|
|
|
|
|
|
prev_start = start;
|
2021-02-12 22:53:01 +01:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
|
2020-06-30 03:47:45 +02:00
|
|
|
// If the two arrays happen to differ in length, just go with the smallest one
|
Remove `VVV_min/max` in favor of `SDL_min/max`
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.
2021-12-23 01:43:31 +01:00
|
|
|
for (size_t i = 0; i < SDL_min(customlevelnames.size(), customlevelscores.size()); i++)
|
2020-06-30 03:47:45 +02:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
const std::string& name = customlevelnames[i];
|
|
|
|
const int score = customlevelscores[i];
|
|
|
|
|
|
|
|
if (customlevelstats.count(name) == 0 || score > customlevelstats[name])
|
|
|
|
{
|
|
|
|
customlevelstats[name] = score;
|
|
|
|
}
|
2020-06-30 03:47:45 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::savecustomlevelstats(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-04 04:32:18 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
2020-09-25 18:09:21 +02:00
|
|
|
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/levelstats.vvv", doc);
|
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No levelstats.vvv found. Creating new file");
|
2020-09-25 18:09:21 +02:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing levelstats.vvv: %s", doc.ErrorStr());
|
|
|
|
vlog_info("Creating new levelstats.vvv");
|
|
|
|
}
|
2020-09-25 18:09:21 +02:00
|
|
|
|
|
|
|
xml::update_declaration(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:09:21 +02:00
|
|
|
tinyxml2::XMLElement * root = xml::update_element(doc, "Levelstats");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:09:21 +02:00
|
|
|
xml::update_comment(root, " Levelstats Save file ");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:09:21 +02:00
|
|
|
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
int numcustomlevelstats = customlevelstats.size();
|
2020-01-01 21:29:24 +01:00
|
|
|
if(numcustomlevelstats>=200)numcustomlevelstats=199;
|
2020-09-25 18:09:21 +02:00
|
|
|
xml::update_tag(msgs, "numcustomlevelstats", numcustomlevelstats);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string customlevelscorestr;
|
|
|
|
std::string customlevelstatsstr;
|
2023-10-25 04:05:23 +02:00
|
|
|
std::map<std::string, int>::iterator iter;
|
|
|
|
for (iter = customlevelstats.begin(); iter != customlevelstats.end(); iter++)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
customlevelscorestr += help.String(iter->second) + ",";
|
|
|
|
customlevelstatsstr += iter->first + "|";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-10-25 04:05:23 +02:00
|
|
|
xml::update_tag(msgs, "customlevelscore", customlevelscorestr.c_str());
|
2020-09-25 18:09:21 +02:00
|
|
|
xml::update_tag(msgs, "customlevelstats", customlevelstatsstr.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
// New system
|
2020-09-25 18:09:21 +02:00
|
|
|
tinyxml2::XMLElement* msg = xml::update_element_delete_contents(msgs, "stats");
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
tinyxml2::XMLElement* stat_el;
|
2023-10-25 04:05:23 +02:00
|
|
|
for (iter = customlevelstats.begin(); iter != customlevelstats.end(); iter++)
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
{
|
|
|
|
stat_el = doc.NewElement("stat");
|
|
|
|
|
2023-10-25 04:05:23 +02:00
|
|
|
stat_el->SetAttribute("name", iter->first.c_str());
|
|
|
|
stat_el->LinkEndChild(doc.NewText(help.String(iter->second).c_str()));
|
Refactor how custom level stats are stored, read, and written
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.
2020-06-30 03:39:22 +02:00
|
|
|
|
|
|
|
msg->LinkEndChild(stat_el);
|
|
|
|
}
|
|
|
|
|
2020-06-04 04:32:18 +02:00
|
|
|
if(FILESYSTEM_saveTiXml2Document("saves/levelstats.vvv", doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("Level stats saved");
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Could Not Save level stats!");
|
|
|
|
vlog_error("Failed: %s%s", saveFilePath, "levelstats.vvv");
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
void Game::levelcomplete_textbox(void)
|
|
|
|
{
|
2023-08-20 19:11:44 +02:00
|
|
|
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("cyan"));
|
2022-12-30 23:38:05 +01:00
|
|
|
graphics.addline(" ");
|
2021-03-20 03:37:54 +01:00
|
|
|
graphics.addline("");
|
|
|
|
graphics.addline("");
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2021-03-20 03:37:54 +01:00
|
|
|
graphics.textboxcenterx();
|
2023-08-20 19:11:44 +02:00
|
|
|
graphics.setimage(TEXTIMAGE_LEVELCOMPLETE);
|
2024-02-01 20:02:06 +01:00
|
|
|
graphics.setlinegap(0);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxapplyposition();
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
static void compute_crewmate_textbox(textboxclass* THIS)
|
2021-03-20 03:37:54 +01:00
|
|
|
{
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->addline("");
|
|
|
|
|
2023-07-12 22:58:47 +02:00
|
|
|
const int extra_cjk_height = (font::height(PR_FONT_INTERFACE) * 4) - 32;
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->yp = 64 + 8 + 16 - extra_cjk_height/2;
|
2022-12-30 23:38:05 +01:00
|
|
|
|
|
|
|
/* This is a special case for wrapping, we MUST have two lines.
|
|
|
|
* So just make sure it can't fit in one line. */
|
|
|
|
const char* text = loc::gettext("You have rescued a crew member!");
|
2023-01-21 02:31:44 +01:00
|
|
|
std::string wrapped = font::string_wordwrap_balanced(PR_FONT_INTERFACE, text, font::len(PR_FONT_INTERFACE, text)-1);
|
2022-12-30 23:38:05 +01:00
|
|
|
|
|
|
|
size_t startline = 0;
|
|
|
|
size_t newline;
|
|
|
|
do {
|
|
|
|
size_t pos_n = wrapped.find('\n', startline);
|
|
|
|
size_t pos_p = wrapped.find('|', startline);
|
|
|
|
newline = SDL_min(pos_n, pos_p);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->addline(wrapped.substr(startline, newline-startline));
|
2022-12-30 23:38:05 +01:00
|
|
|
startline = newline+1;
|
|
|
|
} while (newline != std::string::npos);
|
|
|
|
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->addline("");
|
|
|
|
THIS->centertext();
|
2023-07-12 22:58:47 +02:00
|
|
|
float spaces_per_8 = font::len(PR_FONT_INTERFACE, " ")/8.0f;
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->pad(SDL_ceilf(5/spaces_per_8), SDL_ceilf(2/spaces_per_8));
|
|
|
|
if (!THIS->sprites.empty())
|
|
|
|
{
|
|
|
|
THIS->sprites[0].y = 12 + extra_cjk_height/2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::crewmate_textbox(const int color)
|
|
|
|
{
|
|
|
|
const int extra_cjk_height = (font::height(PR_FONT_INTERFACE) * 4) - 32;
|
|
|
|
graphics.createtextboxflipme("", -1, 64 + 8 + 16, TEXT_COLOUR("gray"));
|
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 03:37:54 +01:00
|
|
|
graphics.textboxcenterx();
|
2023-11-27 23:25:18 +01:00
|
|
|
graphics.addsprite(14, 12 + extra_cjk_height/2, 0, color);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, compute_crewmate_textbox);
|
2024-02-01 20:02:06 +01:00
|
|
|
graphics.setlinegap(0);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxapplyposition();
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
static void compute_remaining_textbox(textboxclass* THIS)
|
2021-03-20 03:37:54 +01:00
|
|
|
{
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
extern Game game;
|
|
|
|
const int remaining = 6 - game.crewrescued();
|
2021-09-13 06:39:07 +02:00
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
2022-12-30 23:38:05 +01:00
|
|
|
if (remaining > 0)
|
2021-03-20 03:37:54 +01:00
|
|
|
{
|
2022-12-30 23:38:05 +01:00
|
|
|
loc::gettext_plural_fill(buffer, sizeof(buffer), "{n_crew|wordy} remain", "{n_crew|wordy} remains", "n_crew:int", remaining);
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-12-30 23:38:05 +01:00
|
|
|
SDL_strlcpy(buffer, loc::gettext("All Crew Members Rescued!"), sizeof(buffer));
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
|
2023-07-12 22:58:47 +02:00
|
|
|
// In CJK, the "You have rescued" box becomes so big we should lower this one a bit...
|
|
|
|
const int cjk_lowering = font::height(PR_FONT_INTERFACE) - 8;
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->yp = 128 + 16 + cjk_lowering;
|
|
|
|
THIS->pad(2, 2);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::remaining_textbox(void)
|
|
|
|
{
|
|
|
|
graphics.createtextboxflipme("", -1, 128 + 16, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 03:37:54 +01:00
|
|
|
graphics.textboxcenterx();
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, compute_remaining_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
static void compute_actionprompt_textbox(textboxclass* THIS)
|
2021-03-20 03:37:54 +01:00
|
|
|
{
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
THIS->lines.clear();
|
|
|
|
|
2023-03-18 22:32:26 +01:00
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
vformat_buf(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
loc::gettext("Press {button} to continue"),
|
|
|
|
"button:but",
|
|
|
|
vformat_button(ActionSet_InGame, Action_InGame_ACTION)
|
|
|
|
);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::actionprompt_textbox(void)
|
|
|
|
{
|
|
|
|
graphics.createtextboxflipme("", -1, 196, TEXT_COLOUR("cyan"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 03:37:54 +01:00
|
|
|
graphics.textboxcenterx();
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, compute_actionprompt_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
2021-03-20 03:37:54 +01:00
|
|
|
}
|
|
|
|
|
2024-01-21 06:33:57 +01:00
|
|
|
static void savetele_textbox_success(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(loc::gettext("Game Saved"));
|
|
|
|
THIS->pad(3, 3);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void savetele_textbox_fail(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(loc::gettext("ERROR: Could not save game!"));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
2021-03-20 03:40:16 +01:00
|
|
|
void Game::savetele_textbox(void)
|
|
|
|
{
|
|
|
|
if (inspecial() || map.custommode)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (savetele())
|
|
|
|
{
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:38:05 +01:00
|
|
|
graphics.textboxcenterx();
|
2021-03-20 03:40:16 +01:00
|
|
|
graphics.textboxtimer(25);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, savetele_textbox_success);
|
2021-03-20 03:40:16 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("red"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:38:05 +01:00
|
|
|
graphics.textboxcenterx();
|
2021-03-20 03:40:16 +01:00
|
|
|
graphics.textboxtimer(50);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, savetele_textbox_fail);
|
|
|
|
}
|
|
|
|
graphics.textboxapplyposition();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void wasd_textbox(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(BUTTONGLYPHS_get_wasd_text());
|
|
|
|
THIS->wrap(4);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(2, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void flip_textbox(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS*3 + 1];
|
|
|
|
vformat_buf(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
loc::gettext("Press {button} to flip"),
|
|
|
|
"button:but",
|
|
|
|
vformat_button(ActionSet_InGame, Action_InGame_ACTION)
|
|
|
|
);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
THIS->wrap(4);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(2, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void arrowkey_textbox(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(loc::gettext("If you prefer, you can press UP or DOWN instead of ACTION to flip."));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void map_textbox(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS*3 + 1];
|
|
|
|
vformat_buf(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
loc::gettext("Press {button} to view map and quicksave"),
|
|
|
|
"button:but",
|
|
|
|
vformat_button(ActionSet_InGame, Action_InGame_Map)
|
|
|
|
);
|
|
|
|
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
THIS->wrap(4);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(2, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void im1_instructions_textbox1(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
// Intermission 1 instructional textbox, depends on last saved
|
|
|
|
const char* floorceiling = graphics.flipmode ? "ceiling" : "floor";
|
|
|
|
const char* crewmate;
|
|
|
|
switch (game.lastsaved)
|
|
|
|
{
|
|
|
|
case 2:
|
|
|
|
crewmate = "Vitellary";
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
crewmate = "Vermilion";
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
crewmate = "Verdigris";
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
crewmate = "Victoria";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
crewmate = "your companion";
|
|
|
|
}
|
|
|
|
char english[SCREEN_WIDTH_TILES*3 + 1]; /* ASCII only */
|
|
|
|
vformat_buf(english, sizeof(english),
|
|
|
|
"When you're standing on the {floorceiling}, {crewmate} will try to walk to you.",
|
|
|
|
"floorceiling:str, crewmate:str",
|
|
|
|
floorceiling, crewmate
|
|
|
|
);
|
|
|
|
|
|
|
|
THIS->lines.push_back(loc::gettext(english));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->padtowidth(36*8);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void im1_instructions_textbox2(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
// Intermission 1 instructional textbox, depends on last saved
|
|
|
|
const char* floorceiling = graphics.flipmode ? "ceiling" : "floor";
|
|
|
|
const char* crewmate;
|
|
|
|
switch (game.lastsaved)
|
|
|
|
{
|
|
|
|
case 2:
|
|
|
|
crewmate = "Vitellary";
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
crewmate = "Vermilion";
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
crewmate = "Verdigris";
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
crewmate = "Victoria";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
crewmate = "your companion";
|
|
|
|
}
|
|
|
|
char english[SCREEN_WIDTH_TILES*3 + 1]; /* ASCII only */
|
|
|
|
vformat_buf(english, sizeof(english),
|
|
|
|
"When you're NOT standing on the {floorceiling}, {crewmate} will stop and wait for you.",
|
|
|
|
"floorceiling:str, crewmate:str",
|
|
|
|
floorceiling, crewmate
|
|
|
|
);
|
|
|
|
|
|
|
|
THIS->lines.push_back(loc::gettext(english));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->padtowidth(36*8);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void im1_instructions_textbox3(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
// Intermission 1 instructional textbox, depends on last saved
|
|
|
|
const char* english;
|
|
|
|
switch (game.lastsaved)
|
|
|
|
{
|
|
|
|
case 2:
|
|
|
|
case 3:
|
|
|
|
case 4:
|
|
|
|
english = "You can't continue to the next room until he is safely across.";
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
english = "You can't continue to the next room until she is safely across.";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
english = "You can't continue to the next room until they are safely across.";
|
2021-03-20 03:40:16 +01:00
|
|
|
}
|
2024-01-21 06:33:57 +01:00
|
|
|
|
|
|
|
THIS->lines.push_back(loc::gettext(english));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->padtowidth(36*8);
|
2021-03-20 03:40:16 +01:00
|
|
|
}
|
|
|
|
|
2024-01-21 21:39:15 +01:00
|
|
|
/* Also used in foundtrinket() script command. */
|
|
|
|
void foundtrinket_textbox1(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
THIS->lines.push_back(loc::gettext("Congratulations!\n\nYou have found a shiny trinket!"));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Also used in foundtrinket() script command. */
|
|
|
|
void foundtrinket_textbox2(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const int max_trinkets = map.custommode ? cl.numtrinkets() : 20;
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
vformat_buf(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
loc::gettext("{n_trinkets|wordy} out of {max_trinkets|wordy}"),
|
|
|
|
"n_trinkets:int, max_trinkets:int",
|
|
|
|
game.trinkets(), max_trinkets
|
|
|
|
);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
|
|
|
|
if (INBOUNDS_VEC(THIS->other_textbox_index, graphics.textboxes)
|
|
|
|
&& &graphics.textboxes[THIS->other_textbox_index] != THIS)
|
|
|
|
{
|
|
|
|
THIS->yp = 95 + graphics.textboxes[THIS->other_textbox_index].h;
|
|
|
|
}
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void foundcrewmate_textbox1(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
THIS->lines.push_back(loc::gettext("Congratulations!\n\nYou have found a lost crewmate!"));
|
|
|
|
THIS->wrap(2);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void foundcrewmate_textbox2(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const int num_remaining = cl.numcrewmates() - game.crewmates();
|
|
|
|
if (num_remaining == 0)
|
|
|
|
{
|
|
|
|
THIS->lines.push_back(loc::gettext("All crewmates rescued!"));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
loc::gettext_plural_fill(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
"{n_crew|wordy} remain", "{n_crew|wordy} remains",
|
|
|
|
"n_crew:int",
|
|
|
|
num_remaining
|
|
|
|
);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (INBOUNDS_VEC(THIS->other_textbox_index, graphics.textboxes)
|
|
|
|
&& &graphics.textboxes[THIS->other_textbox_index] != THIS)
|
|
|
|
{
|
|
|
|
THIS->yp = 95 + graphics.textboxes[THIS->other_textbox_index].h;
|
|
|
|
}
|
|
|
|
THIS->wrap(4);
|
|
|
|
THIS->centertext();
|
|
|
|
THIS->pad(2, 2);
|
|
|
|
}
|
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
static void gamecomplete_textbox2(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
THIS->lines.push_back(loc::gettext("All Crew Members Rescued!"));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox3(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const char* label = loc::gettext("Trinkets Found:");
|
|
|
|
THIS->lines.push_back(label);
|
|
|
|
THIS->xp = 170 - font::len(PR_FONT_INTERFACE, label);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox4(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
vformat_buf(buffer, sizeof(buffer),
|
|
|
|
loc::gettext("{gamecomplete_n_trinkets|wordy}"),
|
|
|
|
"gamecomplete_n_trinkets:int",
|
|
|
|
game.trinkets()
|
|
|
|
);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox5(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const char* label = loc::gettext("Game Time:");
|
|
|
|
THIS->lines.push_back(label);
|
|
|
|
THIS->xp = 170 - font::len(PR_FONT_INTERFACE, label);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox6(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
help.format_time(buffer, sizeof(buffer), game.saveseconds, game.saveframes, true);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox7(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const char* label = loc::gettext("Total Flips:");
|
|
|
|
THIS->lines.push_back(label);
|
|
|
|
THIS->xp = 170 - font::len(PR_FONT_INTERFACE, label);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox9(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
const char* label = loc::gettext("Total Deaths:");
|
|
|
|
THIS->lines.push_back(label);
|
|
|
|
THIS->xp = 170 - font::len(PR_FONT_INTERFACE, label);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox11(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
char buffer[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
loc::gettext_plural_fill(
|
|
|
|
buffer, sizeof(buffer),
|
|
|
|
"Hardest Room (with {n_deaths} deaths)",
|
|
|
|
"Hardest Room (with {n_deaths} death)",
|
|
|
|
"n_deaths:int",
|
|
|
|
game.hardestroomdeaths
|
|
|
|
);
|
|
|
|
THIS->lines.push_back(buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gamecomplete_textbox12(textboxclass* THIS)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
THIS->lines.clear();
|
|
|
|
|
|
|
|
THIS->lines.push_back(
|
|
|
|
loc::gettext_roomname(
|
|
|
|
map.custommode,
|
|
|
|
game.hardestroom_x, game.hardestroom_y,
|
|
|
|
game.hardestroom.c_str(), game.hardestroom_specialname
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-07 18:32:28 +01:00
|
|
|
void Game::setstate(const int gamestate)
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
2024-01-22 22:14:36 +01:00
|
|
|
if (!statelocked || GlitchrunnerMode_less_than_or_equal(Glitchrunner2_2))
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
|
|
|
state = gamestate;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-18 23:24:14 +01:00
|
|
|
void Game::incstate(void)
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
2024-01-22 22:14:36 +01:00
|
|
|
if (!statelocked || GlitchrunnerMode_less_than_or_equal(Glitchrunner2_2))
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
|
|
|
state++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-07 18:32:28 +01:00
|
|
|
void Game::setstatedelay(const int delay)
|
2022-12-07 00:35:06 +01:00
|
|
|
{
|
2024-01-22 22:14:36 +01:00
|
|
|
if (!statelocked || GlitchrunnerMode_less_than_or_equal(Glitchrunner2_2))
|
2022-12-07 00:35:06 +01:00
|
|
|
{
|
|
|
|
statedelay = delay;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-18 23:24:14 +01:00
|
|
|
void Game::lockstate(void)
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
|
|
|
statelocked = true;
|
|
|
|
}
|
|
|
|
|
2023-03-18 23:24:14 +01:00
|
|
|
void Game::unlockstate(void)
|
2022-12-07 00:20:48 +01:00
|
|
|
{
|
|
|
|
statelocked = false;
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::updatestate(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
statedelay--;
|
2022-12-07 18:32:28 +01:00
|
|
|
if (statedelay <= 0)
|
|
|
|
{
|
2022-12-07 00:35:06 +01:00
|
|
|
statedelay = 0;
|
2020-04-02 22:01:55 +02:00
|
|
|
glitchrunkludge=false;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (statedelay <= 0)
|
|
|
|
{
|
|
|
|
switch(state)
|
|
|
|
{
|
|
|
|
case 0:
|
|
|
|
//Do nothing here! Standard game state
|
2020-08-03 00:39:21 +02:00
|
|
|
|
Fix softlocks from turning off advancetext at wrong time
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.
2021-04-11 03:15:43 +02:00
|
|
|
if (script.running)
|
2020-08-03 00:39:21 +02:00
|
|
|
{
|
Fix softlocks from turning off advancetext at wrong time
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.
2021-04-11 03:15:43 +02:00
|
|
|
if (pausescript && !advancetext)
|
|
|
|
{
|
|
|
|
/* Prevent softlocks if we somehow don't have advancetext */
|
|
|
|
pausescript = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-03-19 21:48:01 +01:00
|
|
|
if (completestop)
|
|
|
|
{
|
|
|
|
/* Close potential collection dialogue if warping to ship */
|
|
|
|
graphics.textboxremove();
|
|
|
|
graphics.showcutscenebars = false;
|
|
|
|
}
|
Fix softlocks from turning off advancetext at wrong time
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.
2021-04-11 03:15:43 +02:00
|
|
|
/* Prevent softlocks if there's no cutscene running right now */
|
2020-08-03 00:39:21 +02:00
|
|
|
hascontrol = true;
|
Fix softlocks from mistimed trinket text skip
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.
2021-04-11 00:38:46 +02:00
|
|
|
completestop = false;
|
2020-08-03 00:39:21 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
//Game initilisation
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
//Opening cutscene
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3);
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("To do: write quick", 50, 80, TEXT_COLOUR("cyan"));
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.addline("intro to story!");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2020-01-01 21:29:24 +01:00
|
|
|
//Oh no! what happen to rest of crew etc crash into dimension
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
//End of opening cutscene for now
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 195, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, wasd_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
//Demo over
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
|
|
|
|
|
|
|
startscript = true;
|
|
|
|
newscript="returntohub";
|
|
|
|
obj.removetrigger(5);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(6);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
//End of opening cutscene for now
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 8:
|
|
|
|
//Enter dialogue
|
|
|
|
obj.removetrigger(8);
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[13])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[13] = true;
|
Add support for button glyph display
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).
2023-03-18 22:30:16 +01:00
|
|
|
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 155, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, map_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 9:
|
Move Secret Lab nocompetitive check to Super Gravitron
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.
2021-05-04 03:57:13 +02:00
|
|
|
if (!map.custommode && nocompetitive())
|
|
|
|
{
|
|
|
|
returntolab();
|
2021-09-03 21:08:31 +02:00
|
|
|
|
|
|
|
startscript = true;
|
|
|
|
newscript = "disableaccessibility";
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
Move Secret Lab nocompetitive check to Super Gravitron
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.
2021-05-04 03:57:13 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
//Start SWN Minigame Mode B
|
|
|
|
obj.removetrigger(9);
|
|
|
|
|
|
|
|
swnmode = true;
|
2023-06-05 08:24:31 +02:00
|
|
|
swngame = SWN_START_SUPERGRAVITRON_STEP_1;
|
2020-01-01 21:29:24 +01:00
|
|
|
swndelay = 150;
|
|
|
|
swntimer = 60 * 30;
|
|
|
|
|
|
|
|
//set the checkpoint in the middle of the screen
|
|
|
|
savepoint = 0;
|
|
|
|
savex = 148;
|
|
|
|
savey = 100;
|
|
|
|
savegc = 0;
|
|
|
|
saverx = roomx;
|
|
|
|
savery = roomy;
|
|
|
|
savedir = 0;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 10:
|
|
|
|
//Start SWN Minigame Mode A
|
|
|
|
obj.removetrigger(10);
|
|
|
|
|
|
|
|
swnmode = true;
|
2023-06-05 08:24:31 +02:00
|
|
|
swngame = SWN_START_GRAVITRON_STEP_1;
|
2020-01-01 21:29:24 +01:00
|
|
|
swndelay = 150;
|
|
|
|
swntimer = 60 * 30;
|
|
|
|
|
|
|
|
//set the checkpoint in the middle of the screen
|
|
|
|
savepoint = 0;
|
|
|
|
savex = 148;
|
|
|
|
savey = 100;
|
|
|
|
savegc = 0;
|
|
|
|
saverx = roomx;
|
|
|
|
savery = roomy;
|
|
|
|
savedir = 0;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 11:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremovefast();
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 3, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(180);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, im1_instructions_textbox2);
|
|
|
|
graphics.textboxapplyposition();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 12:
|
|
|
|
obj.removetrigger(12);
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[61])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[61] = true;
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremovefast();
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 3, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(120);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, im1_instructions_textbox3);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 13:
|
|
|
|
//textbox removal
|
|
|
|
obj.removetrigger(13);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremovefast();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 14:
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 3, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(280);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, im1_instructions_textbox1);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 15:
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//leaving the naughty corner
|
2020-06-13 05:36:08 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[obj.getplayer()].tile = 0;
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-06-13 05:36:08 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 16:
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//entering the naughty corner
|
2020-06-13 05:36:08 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if(INBOUNDS_VEC(i, obj.entities) && obj.entities[i].tile == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].tile = 144;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_CRY);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-06-13 05:36:08 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
case 17:
|
|
|
|
//Arrow key tutorial
|
|
|
|
obj.removetrigger(17);
|
Add support for button glyph display
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).
2023-03-18 22:30:16 +01:00
|
|
|
if (BUTTONGLYPHS_keyboard_is_active())
|
|
|
|
{
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 187, TEXT_COLOUR("gray"));
|
Add support for button glyph display
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).
2023-03-18 22:30:16 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
|
|
|
graphics.textboxcenterx();
|
|
|
|
graphics.textboxtimer(100);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, arrowkey_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
Add support for button glyph display
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).
2023-03-18 22:30:16 +01:00
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 20:
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[1])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[1] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(20);
|
|
|
|
break;
|
|
|
|
case 21:
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[2])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[2] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(21);
|
|
|
|
break;
|
|
|
|
case 22:
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[3])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremovefast();
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[3] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2023-03-18 22:32:26 +01:00
|
|
|
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.createtextbox("", -1, 25, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2024-01-21 06:33:57 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, flip_textbox);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(22);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 30:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[4])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[4] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="firststeps";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(30);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 31:
|
2022-12-07 00:35:06 +01:00
|
|
|
//state = 55; setstatedelay(50);
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(0);
|
|
|
|
setstatedelay(0);
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[6])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[6] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[5] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="communicationstation";
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(0);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(31);
|
|
|
|
break;
|
|
|
|
case 32:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[7])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[7] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="teleporterback";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(32);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 33:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[9])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[9] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="rescueblue";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(33);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 34:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[10])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[10] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="rescueyellow";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(34);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 35:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[11])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[11] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="rescuegreen";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(35);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 36:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[8])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[8] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="rescuered";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(36);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 37:
|
|
|
|
//Generic "run script"
|
|
|
|
if (companion == 0)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="int2_yellow";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(37);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 38:
|
|
|
|
//Generic "run script"
|
|
|
|
if (companion == 0)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="int2_red";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(38);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 39:
|
|
|
|
//Generic "run script"
|
|
|
|
if (companion == 0)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="int2_green";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(39);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 40:
|
|
|
|
//Generic "run script"
|
|
|
|
if (companion == 0)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="int2_blue";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(40);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 41:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[60])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[60] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_2";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_2";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_2";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_2";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(41);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 42:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[62])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[62] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_3";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_3";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_3";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_3";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(42);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 43:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[63])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[63] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_4";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_4";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_4";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_4";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(43);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 44:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[64])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[64] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_5";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_5";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_5";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_5";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(44);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 45:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[65])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[65] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_6";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_6";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_6";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_6";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(45);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 46:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[66])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[66] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
|
|
|
newscript = "int1yellow_7";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
|
|
|
newscript = "int1red_7";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
|
|
|
newscript = "int1green_7";
|
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
|
|
|
newscript = "int1blue_7";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(46);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 47:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[69])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[69] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="trenchwarfare";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(47);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 48:
|
|
|
|
//Generic "run script"
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[70])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[70] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript="trinketcollector";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(48);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 49:
|
|
|
|
//Start final level music
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[71])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[71] = true;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.niceplay(Music_PREDESTINEDFATEREMIX);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
obj.removetrigger(49);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 50:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Help! Can anyone hear this message?");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 51:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Verdigris? Are you out there? Are you ok?");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 52:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Please help us! We've crashed and need assistance!");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 53:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Hello? Anyone out there?");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 54:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("This is Doctor Violet from the D.S.S. Souleye! Please respond!");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 55:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Please... Anyone...");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 56:
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIOLET);
|
2024-01-21 20:04:00 +01:00
|
|
|
graphics.createtextbox("", 5, 8, TEXT_COLOUR("purple"));
|
|
|
|
graphics.textboxcommsrelay("Please be alright, everyone...");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxtimer(60);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(50);
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(100);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 80:
|
|
|
|
//Used to return to menu from the game
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 81:
|
2020-05-07 23:38:19 +02:00
|
|
|
quittomenu();
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu()
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 82:
|
|
|
|
//Time Trial Complete!
|
|
|
|
obj.removetrigger(82);
|
2023-05-19 04:58:23 +02:00
|
|
|
if (map.custommode && !map.custommodeforreal)
|
|
|
|
{
|
|
|
|
returntoeditor();
|
|
|
|
ed.show_note(loc::gettext("Time trial completed"));
|
|
|
|
break;
|
|
|
|
}
|
2023-08-23 19:51:11 +02:00
|
|
|
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
if (translator_exploring)
|
|
|
|
{
|
|
|
|
translator_exploring_allowtele = true;
|
2022-12-31 16:01:11 +01:00
|
|
|
setstate(0);
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
break;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = false;
|
2021-01-08 01:02:45 +01:00
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
if (timetrialcheater)
|
|
|
|
{
|
|
|
|
SDL_zeroa(obj.collect);
|
|
|
|
}
|
|
|
|
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
timetrialresulttime = help.hms_to_seconds(hours, minutes, seconds);
|
2020-06-30 00:53:19 +02:00
|
|
|
timetrialresultframes = frames;
|
2021-01-08 01:02:45 +01:00
|
|
|
timetrialresulttrinkets = trinkets();
|
|
|
|
timetrialresultshinytarget = timetrialshinytarget;
|
|
|
|
timetrialresultpar = timetrialpar;
|
|
|
|
timetrialresultdeaths = deathcounts;
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
timetrialrank = 0;
|
|
|
|
if (timetrialresulttime <= timetrialpar) timetrialrank++;
|
2020-04-07 08:46:27 +02:00
|
|
|
if (trinkets() >= timetrialshinytarget) timetrialrank++;
|
2020-01-01 21:29:24 +01:00
|
|
|
if (deathcounts == 0) timetrialrank++;
|
|
|
|
|
2020-07-01 03:59:16 +02:00
|
|
|
if (timetrialresulttime < besttimes[timetriallevel]
|
|
|
|
|| (timetrialresulttime == besttimes[timetriallevel] && timetrialresultframes < bestframes[timetriallevel])
|
|
|
|
|| besttimes[timetriallevel]==-1)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
besttimes[timetriallevel] = timetrialresulttime;
|
2020-07-01 03:59:16 +02:00
|
|
|
bestframes[timetriallevel] = timetrialresultframes;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-01-08 01:02:45 +01:00
|
|
|
if (timetrialresulttrinkets > besttrinkets[timetriallevel] || besttrinkets[timetriallevel]==-1)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-07 08:46:27 +02:00
|
|
|
besttrinkets[timetriallevel] = trinkets();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
if (deathcounts < bestlives[timetriallevel] || bestlives[timetriallevel]==-1)
|
|
|
|
{
|
|
|
|
bestlives[timetriallevel] = deathcounts;
|
|
|
|
}
|
|
|
|
if (timetrialrank > bestrank[timetriallevel] || bestrank[timetriallevel]==-1)
|
|
|
|
{
|
|
|
|
bestrank[timetriallevel] = timetrialrank;
|
2023-06-06 02:50:19 +02:00
|
|
|
if (timetrialrank >= 3)
|
|
|
|
{
|
|
|
|
switch (timetriallevel)
|
|
|
|
{
|
|
|
|
case TimeTrial_SPACESTATION1:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_station1_fixed");
|
|
|
|
break;
|
|
|
|
case TimeTrial_LABORATORY:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_lab_fixed");
|
|
|
|
break;
|
|
|
|
case TimeTrial_TOWER:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_tower_fixed");
|
|
|
|
break;
|
|
|
|
case TimeTrial_SPACESTATION2:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_station2_fixed");
|
|
|
|
break;
|
|
|
|
case TimeTrial_WARPZONE:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_warp_fixed");
|
|
|
|
break;
|
|
|
|
case TimeTrial_FINALLEVEL:
|
|
|
|
unlockAchievement("vvvvvvtimetrial_final_fixed");
|
|
|
|
}
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
savestatsandsettings();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2020-01-01 21:29:24 +01:00
|
|
|
music.fadeout();
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 83:
|
|
|
|
frames--;
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 84:
|
Clean up all exit paths to the menu to use common code
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
2021-01-07 23:20:37 +01:00
|
|
|
quittomenu();
|
2020-04-16 06:53:36 +02:00
|
|
|
createmenu(Menu::timetrialcomplete);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 85:
|
|
|
|
//Cutscene skip version of final level change
|
|
|
|
obj.removetrigger(85);
|
|
|
|
//Init final stretch
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_POSITIVEFORCE);
|
2020-04-09 08:34:26 +02:00
|
|
|
obj.flags[72] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
screenshake = 10;
|
|
|
|
flashlight = 5;
|
|
|
|
map.finalstretch = true;
|
|
|
|
map.warpx = false;
|
|
|
|
map.warpy = false;
|
|
|
|
map.background = 6;
|
|
|
|
|
|
|
|
map.final_colormode = true;
|
|
|
|
map.final_colorframe = 1;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
//From 90-100 are run scripts for the eurogamer expo only, remove later
|
|
|
|
case 90:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_station1";
|
|
|
|
obj.removetrigger(90);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 91:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_lab";
|
|
|
|
obj.removetrigger(91);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 92:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_warp";
|
|
|
|
obj.removetrigger(92);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 93:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_tower";
|
|
|
|
obj.removetrigger(93);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 94:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_station2";
|
|
|
|
obj.removetrigger(94);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 95:
|
|
|
|
//Generic "run script"
|
|
|
|
startscript = true;
|
|
|
|
newscript="startexpolevel_final";
|
|
|
|
obj.removetrigger(95);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 96:
|
|
|
|
//Used to return to gravitron to game
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 97:
|
2020-05-08 00:17:04 +02:00
|
|
|
returntolab();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 100:
|
|
|
|
//
|
|
|
|
// Meeting crewmate in the warpzone
|
|
|
|
//
|
|
|
|
obj.removetrigger(100);
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[4])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[4] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 101:
|
|
|
|
{
|
|
|
|
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = false;
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onroof > 0 && gravitycontrol == 1)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
gravitycontrol = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_UNFLIP);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onground > 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 102:
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
companion = 6;
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:43:19 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 0;
|
|
|
|
obj.entities[i].state = 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.createtextbox("Captain! I've been so worried!", 60, 90, 164, 255, 164);
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VERDIGRIS);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 104:
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("I'm glad you're ok!", 135, 152, TEXT_COLOUR("cyan"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 106:
|
|
|
|
{
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.createtextbox("I've been trying to find a", 74, 70, 164, 255, 164);
|
|
|
|
graphics.addline("way out, but I keep going");
|
|
|
|
graphics.addline("around in circles...");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_CRY);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:43:19 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 54;
|
|
|
|
obj.entities[i].state = 0;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 108:
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Don't worry! I have a", 125, 152, TEXT_COLOUR("cyan"));
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.addline("teleporter key!");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 110:
|
|
|
|
{
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:43:19 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 0;
|
|
|
|
obj.entities[i].state = 1;
|
|
|
|
}
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Follow me!", 185, 154, TEXT_COLOUR("cyan"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 112:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 115:
|
|
|
|
//
|
|
|
|
// Test script for space station, totally delete me!
|
|
|
|
//
|
|
|
|
hascontrol = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 116:
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.createtextbox("Sorry Eurogamers! Teleporting around", 60 - 20, 200, 255, 64, 64);
|
|
|
|
graphics.addline("the map doesn't work in this version!");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxcenterx();
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 118:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 120:
|
|
|
|
//
|
|
|
|
// Meeting crewmate in the space station
|
|
|
|
//
|
|
|
|
obj.removetrigger(120);
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[5])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:46:41 +02:00
|
|
|
obj.flags[5] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 121:
|
|
|
|
{
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = false;
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onground > 0 && gravitycontrol == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
gravitycontrol = 1;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_UNFLIP);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onroof > 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 122:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
companion = 7;
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:43:19 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 6;
|
|
|
|
obj.entities[i].state = 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
|
|
|
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Captain! You're ok!", 60-10, 90-40, TEXT_COLOUR("yellow"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VITELLARY);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 124:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("I've found a teleporter, but", 60-20, 90 - 40, TEXT_COLOUR("yellow"));
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.addline("I can't get it to go anywhere...");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_CRY);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 126:
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("I can help with that!", 125, 152-40, TEXT_COLOUR("cyan"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 128:
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("I have the teleporter", 130, 152-35, TEXT_COLOUR("cyan"));
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.addline("codex for our ship!");
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 130:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Yey! Let's go home!", 60-30, 90-35, TEXT_COLOUR("yellow"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VITELLARY);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:43:19 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 6;
|
|
|
|
obj.entities[i].state = 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 132:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 200:
|
|
|
|
//Init final stretch
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-04-09 08:34:26 +02:00
|
|
|
obj.flags[72] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
screenshake = 10;
|
|
|
|
flashlight = 5;
|
|
|
|
map.finalstretch = true;
|
|
|
|
map.warpx = false;
|
|
|
|
map.warpy = false;
|
|
|
|
map.background = 6;
|
|
|
|
|
|
|
|
map.final_colormode = true;
|
|
|
|
map.final_colorframe = 1;
|
|
|
|
|
|
|
|
startscript = true;
|
|
|
|
newscript="finalterminal_finish";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
Fix the two-frame-delay when entering a room with an "init" script
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.
2020-06-28 02:08:57 +02:00
|
|
|
// WARNING: If updating this code, make sure to update Map.cpp mapclass::twoframedelayfix()
|
2020-01-01 21:29:24 +01:00
|
|
|
case 300:
|
|
|
|
case 301:
|
|
|
|
case 302:
|
|
|
|
case 303:
|
|
|
|
case 304:
|
|
|
|
case 305:
|
|
|
|
case 306:
|
|
|
|
case 307:
|
|
|
|
case 308:
|
|
|
|
case 309:
|
|
|
|
case 310:
|
|
|
|
case 311:
|
|
|
|
case 312:
|
|
|
|
case 313:
|
|
|
|
case 314:
|
|
|
|
case 315:
|
|
|
|
case 316:
|
|
|
|
case 317:
|
|
|
|
case 318:
|
|
|
|
case 319:
|
|
|
|
case 320:
|
|
|
|
case 321:
|
|
|
|
case 322:
|
|
|
|
case 323:
|
|
|
|
case 324:
|
|
|
|
case 325:
|
|
|
|
case 326:
|
|
|
|
case 327:
|
|
|
|
case 328:
|
|
|
|
case 329:
|
|
|
|
case 330:
|
|
|
|
case 331:
|
|
|
|
case 332:
|
|
|
|
case 333:
|
|
|
|
case 334:
|
|
|
|
case 335:
|
|
|
|
case 336:
|
|
|
|
startscript = true;
|
2020-07-10 03:14:18 +02:00
|
|
|
newscript="custom_"+customscript[state - 300];
|
|
|
|
obj.removetrigger(state);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 1000:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.showcutscenebars = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = false;
|
|
|
|
completestop = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 1001:
|
|
|
|
//Found a trinket!
|
|
|
|
advancetext = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.createtextboxflipme("", 50, 85, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 04:07:14 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, foundtrinket_textbox1);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.createtextboxflipme("", 50, 95, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.textboxindex(graphics.textboxes.size() - 2);
|
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, foundtrinket_textbox2);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-07-05 07:55:21 +02:00
|
|
|
case 1002:
|
|
|
|
if (!advancetext)
|
|
|
|
{
|
|
|
|
// Prevent softlocks if we somehow don't have advancetext
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-07-05 07:55:21 +02:00
|
|
|
}
|
|
|
|
break;
|
2020-01-01 21:29:24 +01:00
|
|
|
case 1003:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
completestop = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
Don't check `!muted` when fading music after completion prompt
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).
2021-09-11 00:46:14 +02:00
|
|
|
if (music.currentsong > -1)
|
|
|
|
{
|
|
|
|
music.fadeMusicVolumeIn(3000);
|
|
|
|
}
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.showcutscenebars = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 1010:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.showcutscenebars = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = false;
|
|
|
|
completestop = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 1011:
|
Fix crewmate-found text boxes overlapping in flip mode
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!".
2020-02-16 07:58:42 +01:00
|
|
|
//Found a crewmate!
|
2020-01-01 21:29:24 +01:00
|
|
|
advancetext = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.createtextboxflipme("", 50, 85, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 04:07:14 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, foundcrewmate_textbox1);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.createtextboxflipme("", 50, 95, TEXT_COLOUR("gray"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
2021-03-20 04:07:14 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 21:39:15 +01:00
|
|
|
graphics.textboxindex(graphics.textboxes.size() - 2);
|
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, foundcrewmate_textbox2);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-07-05 07:55:21 +02:00
|
|
|
case 1012:
|
|
|
|
if (!advancetext)
|
|
|
|
{
|
|
|
|
// Prevent softlocks if we somehow don't have advancetext
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-07-05 07:55:21 +02:00
|
|
|
}
|
|
|
|
break;
|
2020-01-01 21:29:24 +01:00
|
|
|
case 1013:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
completestop = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-02-21 00:40:11 +01:00
|
|
|
if(cl.numcrewmates()-crewmates()==0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
if(map.custommodeforreal)
|
|
|
|
{
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(1014);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
Remove game.shouldreturntoeditor in favor of using defer callback
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.
2021-01-10 23:48:25 +01:00
|
|
|
returntoeditor();
|
2023-05-19 04:58:23 +02:00
|
|
|
ed.show_note(loc::gettext("Level completed"));
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
Don't check `!muted` when fading music after completion prompt
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).
2021-09-11 00:46:14 +02:00
|
|
|
if (cl.levmusic > 0)
|
|
|
|
{
|
|
|
|
music.fadeMusicVolumeIn(3000);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.showcutscenebars = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 1014:
|
|
|
|
frames--;
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 1015:
|
|
|
|
//Update level stats
|
2022-08-04 02:30:23 +02:00
|
|
|
/* FIXME: Have to add check to not save stats for the dumb hack
|
|
|
|
* `special/stdin.vvvvvv` filename... see elsewhere, grep for
|
|
|
|
* `special/stdin`! */
|
|
|
|
if(cl.numcrewmates()-crewmates()==0 && customlevelfilename != "levels/special/stdin.vvvvvv")
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//Finished level
|
2021-02-21 00:40:11 +01:00
|
|
|
if (trinkets() >= cl.numtrinkets())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//and got all the trinkets!
|
|
|
|
updatecustomlevelstats(customlevelfilename, 3);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
updatecustomlevelstats(customlevelfilename, 1);
|
|
|
|
}
|
|
|
|
}
|
2023-08-23 19:51:11 +02:00
|
|
|
|
2020-08-01 22:01:58 +02:00
|
|
|
quittomenu();
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu()
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 2000:
|
|
|
|
//Game Saved!
|
2021-03-20 03:40:16 +01:00
|
|
|
savetele_textbox();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 2500:
|
|
|
|
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PAUSE);
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter (appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 2501:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
|
|
|
//we're done here!
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 2502:
|
2020-06-13 04:31:08 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 04:31:08 +02:00
|
|
|
{
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
}
|
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
2020-06-13 04:31:08 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
i = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 04:31:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 1;
|
|
|
|
obj.entities[i].colour = 101;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-06-13 04:31:08 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2503:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2504:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
//obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2505:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2506:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2507:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2508:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 2;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2509:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 2510:
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Hello?", 125+24, 152-20, TEXT_COLOUR("cyan"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 2512:
|
|
|
|
advancetext = true;
|
|
|
|
hascontrol = false;
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextbox("Is anyone there?", 125+8, 152-24, TEXT_COLOUR("cyan"));
|
2024-01-21 06:43:46 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_VIRIDIAN);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxactive();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 2514:
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_POTENTIALFORANYTHING);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 3000:
|
|
|
|
//Activating a teleporter (long version for level complete)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3001:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3002:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3003:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3004:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
|
|
|
//we're done here!
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3005:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(50);
|
2020-01-01 21:29:24 +01:00
|
|
|
switch(companion)
|
|
|
|
{
|
|
|
|
case 6:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3006);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Warp Zone
|
|
|
|
case 7:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3020);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Space Station
|
|
|
|
case 8:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3040);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Lab
|
|
|
|
case 9:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3060);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Tower
|
|
|
|
case 10:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3080);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Intermission 2
|
|
|
|
case 11:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3085);
|
2020-01-01 21:29:24 +01:00
|
|
|
break; //Intermission 1
|
|
|
|
}
|
|
|
|
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
if (translator_exploring_allowtele)
|
|
|
|
{
|
2022-12-31 16:01:11 +01:00
|
|
|
setstate(3090);
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
}
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-04-02 00:32:21 +02:00
|
|
|
i = obj.getcompanion();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if(INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Fix entity and block indices after destroying them
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.
2020-12-27 07:11:34 +01:00
|
|
|
obj.disableentity(i);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
i = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 04:31:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 1;
|
|
|
|
obj.entities[i].colour = 100;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
case 3006:
|
|
|
|
//Level complete! (warp zone)
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_WARPZONE_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
lastsaved = 4;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PATHCOMPLETE);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
levelcomplete_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3007:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-11 02:41:39 +02:00
|
|
|
crewmate_textbox(13);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3008:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
remaining_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3009:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3010:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3011:
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(3070);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 3020:
|
|
|
|
//Level complete! (Space Station 2)
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_SPACESTATION2_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
lastsaved = 2;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PATHCOMPLETE);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
levelcomplete_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3021:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-11 02:41:39 +02:00
|
|
|
crewmate_textbox(14);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3022:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
remaining_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3023:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3024:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3025:
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(3070);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 3040:
|
|
|
|
//Level complete! (Lab)
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_LABORATORY_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
lastsaved = 5;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PATHCOMPLETE);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
levelcomplete_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3041:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-11 02:41:39 +02:00
|
|
|
crewmate_textbox(16);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3042:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
remaining_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3043:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3044:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3045:
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(3070);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 3050:
|
|
|
|
//Level complete! (Space Station 1)
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_SPACESTATION1_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
lastsaved = 1;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PATHCOMPLETE);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
levelcomplete_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3051:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-11 02:41:39 +02:00
|
|
|
crewmate_textbox(20);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3052:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
remaining_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3053:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3054:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
teleportscript = "";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3055:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(10);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3056:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
startscript = true;
|
Fix unwinnable save from rescuing Violet out of order
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.
2020-08-09 01:09:55 +02:00
|
|
|
if (crewrescued() == 6)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Fix unwinnable save from rescuing Violet out of order
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.
2020-08-09 01:09:55 +02:00
|
|
|
newscript = "startlevel_final";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
Fix unwinnable save from rescuing Violet out of order
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.
2020-08-09 01:09:55 +02:00
|
|
|
if (nocutscenes)
|
|
|
|
{
|
|
|
|
newscript="bigopenworldskip";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
newscript = "bigopenworld";
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 3060:
|
|
|
|
//Level complete! (Tower)
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_TOWER_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
lastsaved = 3;
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PATHCOMPLETE);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
levelcomplete_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3061:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-11 02:41:39 +02:00
|
|
|
crewmate_textbox(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3062:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
remaining_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3063:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3064:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3065:
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(3070);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 3070:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3071:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3072:
|
|
|
|
//Ok, we need to adjust some flags based on who've we've rescued. Some of there conversation options
|
|
|
|
//change depending on when they get back to the ship.
|
|
|
|
if (lastsaved == 2)
|
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
if (crewstats[3]) obj.flags[25] = true;
|
|
|
|
if (crewstats[4]) obj.flags[26] = true;
|
|
|
|
if (crewstats[5]) obj.flags[24] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (lastsaved == 3)
|
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
if (crewstats[2]) obj.flags[50] = true;
|
|
|
|
if (crewstats[4]) obj.flags[49] = true;
|
|
|
|
if (crewstats[5]) obj.flags[48] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (lastsaved == 4)
|
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
if (crewstats[2]) obj.flags[54] = true;
|
|
|
|
if (crewstats[3]) obj.flags[55] = true;
|
|
|
|
if (crewstats[5]) obj.flags[56] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (lastsaved == 5)
|
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
if (crewstats[2]) obj.flags[37] = true;
|
|
|
|
if (crewstats[3]) obj.flags[38] = true;
|
|
|
|
if (crewstats[4]) obj.flags[39] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
//We're pitch black now, make a decision
|
|
|
|
companion = 0;
|
|
|
|
if (crewrescued() == 6)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="startlevel_final";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (crewrescued() == 4)
|
|
|
|
{
|
|
|
|
companion = 11;
|
|
|
|
supercrewmate = true;
|
|
|
|
scmprogress = 0;
|
|
|
|
|
|
|
|
startscript = true;
|
|
|
|
newscript = "intermission_1";
|
2020-04-09 08:34:26 +02:00
|
|
|
obj.flags[19] = true;
|
|
|
|
if (lastsaved == 2) obj.flags[32] = true;
|
|
|
|
if (lastsaved == 3) obj.flags[35] = true;
|
|
|
|
if (lastsaved == 4) obj.flags[34] = true;
|
|
|
|
if (lastsaved == 5) obj.flags[33] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (crewrescued() == 5)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript = "intermission_2";
|
2020-04-09 08:34:26 +02:00
|
|
|
obj.flags[20] = true;
|
|
|
|
if (lastsaved == 2) obj.flags[32] = true;
|
|
|
|
if (lastsaved == 3) obj.flags[35] = true;
|
|
|
|
if (lastsaved == 4) obj.flags[34] = true;
|
|
|
|
if (lastsaved == 5) obj.flags[33] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript="regularreturn";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 3080:
|
|
|
|
//returning from an intermission, very like 3070
|
|
|
|
if (inintermission)
|
|
|
|
{
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2020-01-01 21:29:24 +01:00
|
|
|
companion = 0;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3100);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_INTERMISSION2_COMPLETE);
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2020-01-01 21:29:24 +01:00
|
|
|
companion = 0;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3081:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3082:
|
|
|
|
map.finalmode = false;
|
|
|
|
startscript = true;
|
|
|
|
newscript="regularreturn";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 3085:
|
|
|
|
//returning from an intermission, very like 3070
|
|
|
|
//return to menu from here
|
|
|
|
if (inintermission)
|
|
|
|
{
|
|
|
|
companion = 0;
|
|
|
|
supercrewmate = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2020-01-01 21:29:24 +01:00
|
|
|
music.fadeout();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(3100);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(Unlock_INTERMISSION1_COMPLETE);
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2020-01-01 21:29:24 +01:00
|
|
|
companion = 0;
|
|
|
|
supercrewmate = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3086:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3087:
|
|
|
|
map.finalmode = false;
|
|
|
|
startscript = true;
|
|
|
|
newscript="regularreturn";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
case 3090:
|
|
|
|
/* Teleporting in translator_exploring should be just like
|
|
|
|
* the intermission replays: simply return to the menu */
|
|
|
|
companion = 0;
|
|
|
|
supercrewmate = false;
|
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
|
|
|
music.fadeout();
|
2022-12-31 16:01:11 +01:00
|
|
|
setstate(3100);
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
break;
|
|
|
|
case 3091:
|
|
|
|
/* Different Final Level ending for translator_exploring */
|
|
|
|
music.fadeout();
|
2022-12-31 16:01:11 +01:00
|
|
|
incstate();
|
|
|
|
setstatedelay(60);
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
break;
|
|
|
|
case 3092:
|
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-31 16:01:11 +01:00
|
|
|
setstate(3100);
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
break;
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
case 3100:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3101:
|
Clean up all exit paths to the menu to use common code
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
2021-01-07 23:20:37 +01:00
|
|
|
quittomenu();
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu();
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 3500:
|
|
|
|
music.fadeout();
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(120);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3501:
|
|
|
|
//Game complete!
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvgamecomplete");
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(UnlockTrophy_GAME_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
crewstats[0] = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(75);
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PLENARY);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2023-08-20 19:11:44 +02:00
|
|
|
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("cyan"));
|
2022-12-30 23:35:26 +01:00
|
|
|
graphics.addline(" ");
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.addline("");
|
|
|
|
graphics.addline("");
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_8X8);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxcenterx();
|
2023-08-20 19:11:44 +02:00
|
|
|
graphics.setimage(TEXTIMAGE_GAMECOMPLETE);
|
2024-02-01 20:02:06 +01:00
|
|
|
graphics.setlinegap(0);
|
Save special text box state using functions
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.
2024-01-21 05:27:31 +01:00
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3502:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-08 00:58:18 +01:00
|
|
|
setstatedelay(45+15);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", -1, 64, TEXT_COLOUR("transparent"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
Fix centering of game complete texts if level font != interface font
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.
2023-12-05 04:00:47 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox2);
|
|
|
|
graphics.textboxapplyposition();
|
|
|
|
saveframes = frames;
|
|
|
|
saveseconds = help.hms_to_seconds(hours, minutes, seconds);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3503:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", 170, 84, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox3);
|
|
|
|
graphics.textboxapplyposition();
|
|
|
|
graphics.createtextboxflipme("", 180, 84, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox4);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3504:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-08 00:58:18 +01:00
|
|
|
setstatedelay(45+15);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", 170, 96, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox5);
|
|
|
|
graphics.textboxapplyposition();
|
|
|
|
graphics.createtextboxflipme("", 180, 96, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox6);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3505:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(45);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", 170, 123, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox7);
|
|
|
|
graphics.textboxapplyposition();
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextboxflipme(help.String(totalflips), 180, 123, TEXT_COLOUR("transparent"));
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3506:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-08 00:58:18 +01:00
|
|
|
setstatedelay(45+15);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", 170, 135, TEXT_COLOUR("transparent"));
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox9);
|
|
|
|
graphics.textboxapplyposition();
|
2023-03-18 23:11:49 +01:00
|
|
|
graphics.createtextboxflipme(help.String(deathcounts), 180, 135, TEXT_COLOUR("transparent"));
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxoriginalcontextauto();
|
2024-01-05 02:39:14 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE | PR_RTL_XFLIP);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3507:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-08 00:58:18 +01:00
|
|
|
setstatedelay(45+15);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.createtextboxflipme("", -1, 158, TEXT_COLOUR("transparent"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
Fix centering of game complete texts if level font != interface font
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.
2023-12-05 04:00:47 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox11);
|
|
|
|
graphics.textboxapplyposition();
|
|
|
|
graphics.createtextboxflipme("", -1, 170, TEXT_COLOUR("transparent"));
|
2023-01-13 05:11:39 +01:00
|
|
|
graphics.textboxprintflags(PR_FONT_INTERFACE);
|
Fix centering of game complete texts if level font != interface font
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.
2023-12-05 04:00:47 +01:00
|
|
|
graphics.textboxcenterx();
|
2024-01-21 22:41:40 +01:00
|
|
|
graphics.textboxtranslate(TEXTTRANSLATE_FUNCTION, gamecomplete_textbox12);
|
|
|
|
graphics.textboxapplyposition();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3508:
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 03:37:54 +01:00
|
|
|
actionprompt_textbox();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3509:
|
|
|
|
if (jumppressed)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-03-31 02:16:02 +02:00
|
|
|
graphics.textboxremove();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3510:
|
|
|
|
//Save stats and stuff here
|
2020-04-09 08:34:26 +02:00
|
|
|
if (!obj.flags[73])
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//flip mode complete
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvgamecompleteflip");
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(UnlockTrophy_FLIPMODE_COMPLETE);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 04:30:28 +02:00
|
|
|
#ifndef MAKEANDPLAY
|
|
|
|
if (!map.custommode)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-05-04 04:30:28 +02:00
|
|
|
if (bestgamedeaths == -1)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
bestgamedeaths = deathcounts;
|
|
|
|
}
|
2021-05-04 04:30:28 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if (deathcounts < bestgamedeaths)
|
|
|
|
{
|
|
|
|
bestgamedeaths = deathcounts;
|
|
|
|
}
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-05-04 04:30:28 +02:00
|
|
|
#endif
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-04-02 22:01:55 +02:00
|
|
|
if (bestgamedeaths > -1) {
|
|
|
|
if (bestgamedeaths <= 500) {
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvcomplete500");
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
|
|
|
if (bestgamedeaths <= 250) {
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvcomplete250");
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
|
|
|
if (bestgamedeaths <= 100) {
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvcomplete100");
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
|
|
|
if (bestgamedeaths <= 50) {
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvcomplete50");
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
|
|
|
}
|
2020-04-02 21:41:33 +02:00
|
|
|
|
2024-08-30 19:19:27 +02:00
|
|
|
if (nodeathmode || nodeatheligible)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-01 21:49:07 +02:00
|
|
|
unlockAchievement("vvvvvvmaster"); //bloody hell
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknum(UnlockTrophy_NODEATHMODE_COMPLETE);
|
2024-08-30 19:19:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (nodeathmode)
|
|
|
|
{
|
2022-12-08 02:10:12 +01:00
|
|
|
setstate(3520);
|
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(120);
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-05-22 08:01:10 +02:00
|
|
|
|
|
|
|
savestatsandsettings();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3511:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter (long version for level complete)
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].colour = 102;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 3512:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3513:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3514:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3515:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
//we're done here!
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(60);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 3516:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3517:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3518:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEIN;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(30);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
map.finalmode = false;
|
|
|
|
map.final_colormode = false;
|
|
|
|
map.final_mapcol = 0;
|
|
|
|
map.final_colorframe = 0;
|
|
|
|
map.finalstretch = false;
|
2023-02-18 00:52:57 +01:00
|
|
|
obj.flags[72] = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-20 07:08:35 +01:00
|
|
|
graphics.setbars(320);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
teleport_to_new_area = true;
|
|
|
|
teleportscript = "gamecomplete";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 3520:
|
|
|
|
//NO DEATH MODE COMPLETE JESUS
|
|
|
|
hascontrol = false;
|
|
|
|
crewstats[0] = true;
|
|
|
|
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEOUT;
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3521:
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
if (graphics.fademode == FADE_FULLY_BLACK)
|
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 3522:
|
2021-01-08 01:18:07 +01:00
|
|
|
copyndmresults();
|
Clean up all exit paths to the menu to use common code
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
2021-01-07 23:20:37 +01:00
|
|
|
quittomenu();
|
2020-04-16 06:53:36 +02:00
|
|
|
createmenu(Menu::nodeathmodecomplete);
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4000:
|
|
|
|
//Activating a teleporter (short version)
|
2022-12-07 00:20:48 +01:00
|
|
|
state++; // Increment manually -- gamestate modification might be locked at this point
|
2020-01-01 21:29:24 +01:00
|
|
|
statedelay = 10;
|
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 10;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4001:
|
|
|
|
//Activating a teleporter 2
|
|
|
|
state++;
|
|
|
|
statedelay = 0;
|
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
|
|
|
//we're done here!
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4002:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
|
|
|
state++;
|
|
|
|
statedelay = 10;
|
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
i = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if(INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
obj.entities[i].tile = 1;
|
|
|
|
obj.entities[i].colour = 100;
|
|
|
|
}
|
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4003:
|
2022-12-08 02:10:12 +01:00
|
|
|
state = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
statedelay = 0;
|
|
|
|
teleport_to_new_area = true;
|
2022-12-07 00:20:48 +01:00
|
|
|
unlockstate();
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4010:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4011:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4012:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4013:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4014:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4015:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4016:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4017:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 3;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4018:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4019:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
if (intimetrial || nodeathmode || inintermission)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-03-31 02:16:02 +02:00
|
|
|
savetele();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getteleporter();
|
2020-01-01 21:29:24 +01:00
|
|
|
activetele = true;
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 04:31:08 +02:00
|
|
|
{
|
|
|
|
teleblock.x = obj.entities[i].xp - 32;
|
|
|
|
teleblock.y = obj.entities[i].yp - 32;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
teleblock.w = 160;
|
|
|
|
teleblock.h = 160;
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
case 4020:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4021:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4022:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4023:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 12;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4024:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 12;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4025:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4026:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4027:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 5;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4028:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 2;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4029:
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4030:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4031:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4032:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = -6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = -6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4033:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 12;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4034:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 12;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4035:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4036:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4037:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 5;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4038:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 2;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4039:
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4040:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4041:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4042:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4043:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 12;
|
|
|
|
obj.entities[i].yp -= 15;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4044:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 12;
|
|
|
|
obj.entities[i].yp -= 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4045:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 12;
|
|
|
|
obj.entities[i].yp -= 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4046:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4047:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4048:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 3;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4049:
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4050:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4051:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4052:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4053:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 4;
|
|
|
|
obj.entities[i].yp -= 15;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4054:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 4;
|
|
|
|
obj.entities[i].yp -= 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4055:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 4;
|
|
|
|
obj.entities[i].yp -= 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4056:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 4;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4057:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 2;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4058:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4059:
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4060:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4061:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4062:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = -6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = -6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4063:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 28;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4064:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 28;
|
|
|
|
obj.entities[i].yp -= 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4065:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 25;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4066:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 25;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4067:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 20;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4068:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp -= 16;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4069:
|
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 4070:
|
|
|
|
//Activating a teleporter (special for final script, player has colour changed to match rescued crewmate)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4071:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4072:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
Refactor colors in internal commands
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.
2021-09-01 00:09:51 +02:00
|
|
|
obj.entities[i].colour = graphics.crewcolour(lastsaved);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4073:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4074:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4075:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4076:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4077:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 3;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4078:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4079:
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
startscript = true;
|
|
|
|
newscript = "finallevel_teleporter";
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 4080:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4081:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4082:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4083:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4084:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4085:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4086:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4087:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 3;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4088:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4089:
|
|
|
|
startscript = true;
|
|
|
|
newscript = "gamecomplete_ending";
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 4090:
|
|
|
|
//Activating a teleporter (default appear)
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 90;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_FLASH);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4091:
|
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
flashlight = 5;
|
|
|
|
screenshake = 0;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_TELEPORT);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
case 4092:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Activating a teleporter 2
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(5);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
|
|
|
int j = obj.getteleporter();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(j, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp = obj.entities[j].xp+44;
|
|
|
|
obj.entities[i].yp = obj.entities[j].yp+44;
|
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp
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.
2020-10-10 05:58:58 +02:00
|
|
|
obj.entities[i].lerpoldxp = obj.entities[i].xp;
|
|
|
|
obj.entities[i].lerpoldyp = obj.entities[i].yp;
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[j].tile = 2;
|
|
|
|
obj.entities[j].colour = 101;
|
|
|
|
}
|
|
|
|
obj.entities[i].colour = 0;
|
|
|
|
obj.entities[i].invis = false;
|
|
|
|
obj.entities[i].dir = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].ay = -6;
|
|
|
|
obj.entities[i].ax = 6;
|
|
|
|
obj.entities[i].vy = -6;
|
|
|
|
obj.entities[i].vx = 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4093:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4094:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 10;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4095:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 8;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4096:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 6;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4097:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 3;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4098:
|
2020-09-08 09:56:00 +02:00
|
|
|
{
|
2022-12-07 00:20:48 +01:00
|
|
|
incstate();
|
2022-12-07 00:35:06 +01:00
|
|
|
setstatedelay(15);
|
2020-09-08 09:56:00 +02:00
|
|
|
int i = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].xp += 1;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
2020-09-08 09:56:00 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
case 4099:
|
|
|
|
if (nocutscenes)
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript = "levelonecompleteskip";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
startscript = true;
|
|
|
|
newscript = "levelonecomplete_ending";
|
|
|
|
}
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::gethardestroom(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
if (currentroomdeaths > hardestroomdeaths)
|
|
|
|
{
|
|
|
|
hardestroomdeaths = currentroomdeaths;
|
2023-02-18 00:52:57 +01:00
|
|
|
|
2023-09-04 15:27:29 +02:00
|
|
|
hardestroom_x = roomx;
|
|
|
|
hardestroom_y = roomy;
|
|
|
|
hardestroom_finalstretch = map.finalstretch;
|
|
|
|
|
2023-02-18 00:52:57 +01:00
|
|
|
if (map.roomname[0] == '\0')
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-09-04 15:27:29 +02:00
|
|
|
hardestroom = map.hiddenname;
|
|
|
|
hardestroom_specialname = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-02-18 00:52:57 +01:00
|
|
|
else
|
2020-02-04 04:27:49 +01:00
|
|
|
{
|
2023-09-04 15:27:29 +02:00
|
|
|
hardestroom = map.roomname;
|
|
|
|
hardestroom_specialname = map.roomname_special;
|
2020-02-04 04:27:49 +01:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::deletestats(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-26 22:38:24 +02:00
|
|
|
if (!FILESYSTEM_delete("saves/unlock.vvv"))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting saves/unlock.vvv");
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-04-26 22:38:24 +02:00
|
|
|
else
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-07-03 01:45:22 +02:00
|
|
|
for (int i = 0; i < numunlock; i++)
|
2020-04-26 22:38:24 +02:00
|
|
|
{
|
|
|
|
unlock[i] = false;
|
|
|
|
unlocknotify[i] = false;
|
|
|
|
}
|
2020-07-03 02:09:30 +02:00
|
|
|
for (int i = 0; i < numtrials; i++)
|
2020-04-26 22:38:24 +02:00
|
|
|
{
|
|
|
|
besttimes[i] = -1;
|
2020-07-01 03:59:16 +02:00
|
|
|
bestframes[i] = -1;
|
2020-04-26 22:38:24 +02:00
|
|
|
besttrinkets[i] = -1;
|
|
|
|
bestlives[i] = -1;
|
|
|
|
bestrank[i] = -1;
|
|
|
|
}
|
2021-09-11 03:02:52 +02:00
|
|
|
swnrecord = 0;
|
|
|
|
swnbestrank = 0;
|
2021-12-18 08:30:51 +01:00
|
|
|
bestgamedeaths = -1;
|
2020-11-04 06:56:18 +01:00
|
|
|
#ifndef MAKEANDPLAY
|
2020-04-26 22:38:24 +02:00
|
|
|
graphics.setflipmode = false;
|
2020-11-04 06:56:18 +01:00
|
|
|
#endif
|
2020-04-26 22:38:24 +02:00
|
|
|
stat_trinkets = 0;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::deletesettings(void)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
if (!FILESYSTEM_delete("saves/settings.vvv"))
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting saves/settings.vvv");
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
void Game::unlocknum( int t )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-02-04 09:14:04 +01:00
|
|
|
#ifdef MAKEANDPLAY
|
|
|
|
UNUSED(t);
|
|
|
|
#else
|
2020-03-15 16:17:12 +01:00
|
|
|
if (map.custommode)
|
|
|
|
{
|
|
|
|
//Don't let custom levels unlock things!
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
unlock[t] = true;
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
savestatsandsettings();
|
2020-08-01 21:49:07 +02:00
|
|
|
#endif
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2022-03-14 06:39:09 +01:00
|
|
|
static bool stats_loaded = false;
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
void Game::loadstats(struct ScreenSettings* screen_settings)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-03 19:11:37 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
tinyxml2::XMLElement* dataNode;
|
|
|
|
|
2022-03-14 06:39:09 +01:00
|
|
|
stats_loaded = true;
|
|
|
|
|
2020-06-03 19:11:37 +02:00
|
|
|
if (!FILESYSTEM_loadTiXml2Document("saves/unlock.vvv", doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
// Save unlock.vvv only. Maybe we have a settings.vvv laying around too,
|
|
|
|
// and we don't want to overwrite that!
|
2020-12-22 01:40:46 +01:00
|
|
|
savestats(screen_settings);
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
return;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_error("Error parsing unlock.vvv: %s", doc.ErrorStr());
|
|
|
|
return;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
dataNode = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = dataNode; pElem != NULL; pElem=pElem->NextSiblingElement())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-03-06 19:31:17 +01:00
|
|
|
const char* pKey = pElem->Value();
|
2020-01-01 21:29:24 +01:00
|
|
|
const char* pText = pElem->GetText() ;
|
|
|
|
|
2021-03-24 20:19:44 +01:00
|
|
|
if (pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
|
|
|
|
2020-07-03 01:58:58 +02:00
|
|
|
LOAD_ARRAY(unlock)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 01:58:58 +02:00
|
|
|
LOAD_ARRAY(unlocknotify)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 02:09:30 +02:00
|
|
|
LOAD_ARRAY(besttimes)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 01:58:58 +02:00
|
|
|
LOAD_ARRAY(bestframes)
|
2020-07-01 03:59:16 +02:00
|
|
|
|
2020-07-03 02:09:30 +02:00
|
|
|
LOAD_ARRAY(besttrinkets)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-07-03 02:09:30 +02:00
|
|
|
LOAD_ARRAY(bestlives)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-07-03 02:09:30 +02:00
|
|
|
LOAD_ARRAY(bestrank)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "bestgamedeaths") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
bestgamedeaths = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "stat_trinkets") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
stat_trinkets = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "swnbestrank") == 0)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
swnbestrank = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "swnrecord") == 0)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
swnrecord = help.Int(pText);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-13 01:45:51 +01:00
|
|
|
deserializesettings(dataNode, screen_settings);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
void Game::deserializesettings(tinyxml2::XMLElement* dataNode, struct ScreenSettings* screen_settings)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
// Don't duplicate controller buttons!
|
|
|
|
controllerButton_flip.clear();
|
|
|
|
controllerButton_map.clear();
|
|
|
|
controllerButton_esc.clear();
|
|
|
|
controllerButton_restart.clear();
|
2021-04-19 08:23:44 +02:00
|
|
|
controllerButton_interact.clear();
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
|
|
|
for (tinyxml2::XMLElement* pElem = dataNode;
|
|
|
|
pElem != NULL;
|
|
|
|
pElem = pElem->NextSiblingElement())
|
|
|
|
{
|
2021-03-06 19:31:17 +01:00
|
|
|
const char* pKey = pElem->Value();
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
const char* pText = pElem->GetText();
|
|
|
|
|
2021-03-24 20:19:44 +01:00
|
|
|
if (pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "fullscreen") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->fullscreen = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "stretch") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
2024-03-30 04:16:57 +01:00
|
|
|
int mode = help.Int(pText);
|
|
|
|
if (mode < 0 || mode >= NUM_SCALING_MODES)
|
|
|
|
{
|
|
|
|
/* Pick a sane default. */
|
|
|
|
mode = SCALING_INTEGER;
|
|
|
|
}
|
|
|
|
screen_settings->scalingMode = mode;
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "useLinearFilter") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->linearFilter = help.Int(pText);
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Store display index of window to settings
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.
2023-03-21 08:23:42 +01:00
|
|
|
if (SDL_strcmp(pKey, "window_display") == 0)
|
|
|
|
{
|
|
|
|
screen_settings->windowDisplay = help.Int(pText);
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "window_width") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->windowWidth = help.Int(pText);
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "window_height") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->windowHeight = help.Int(pText);
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "noflashingmode") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
noflashingmode = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "colourblindmode") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
colourblindmode = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "setflipmode") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
graphics.setflipmode = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "invincibility") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.invincibility = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "slowdown") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
slowdown = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "advanced_smoothing") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->badSignal = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "usingmmmmmm") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-11-13 01:48:39 +01:00
|
|
|
music.usingmmmmmm = (bool) help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "ghostsenabled") == 0)
|
Add a player trail to the editor (ghosts)
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)
2020-06-13 00:04:35 +02:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
ghostsenabled = help.Int(pText);
|
Add a player trail to the editor (ghosts)
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)
2020-06-13 00:04:35 +02:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "skipfakeload") == 0)
|
2020-01-13 02:45:44 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
skipfakeload = help.Int(pText);
|
2020-01-13 02:45:44 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "disablepause") == 0)
|
2020-06-30 04:49:14 +02:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
disablepause = help.Int(pText);
|
2020-06-30 04:49:14 +02:00
|
|
|
}
|
|
|
|
|
2021-08-05 21:20:05 +02:00
|
|
|
if (SDL_strcmp(pKey, "disableaudiopause") == 0)
|
|
|
|
{
|
|
|
|
disableaudiopause = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "over30mode") == 0)
|
2020-05-04 21:52:57 +02:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
over30mode = help.Int(pText);
|
2020-05-04 21:52:57 +02:00
|
|
|
}
|
|
|
|
|
2021-04-02 00:39:56 +02:00
|
|
|
if (SDL_strcmp(pKey, "inputdelay") == 0)
|
|
|
|
{
|
|
|
|
inputdelay = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "glitchrunnermode") == 0)
|
2020-06-25 23:31:37 +02:00
|
|
|
{
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
GlitchrunnerMode_set(GlitchrunnerMode_string_to_enum(pText));
|
2020-06-25 23:31:37 +02:00
|
|
|
}
|
|
|
|
|
2021-08-05 23:31:20 +02:00
|
|
|
if (SDL_strcmp(pKey, "showingametimer") == 0)
|
|
|
|
{
|
|
|
|
showingametimer = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "vsync") == 0)
|
2020-05-04 22:19:47 +02:00
|
|
|
{
|
2020-11-13 01:45:51 +01:00
|
|
|
screen_settings->useVsync = help.Int(pText);
|
2020-05-04 22:19:47 +02:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "notextoutline") == 0)
|
2020-01-17 18:37:53 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
graphics.notextoutline = help.Int(pText);
|
2020-01-17 18:37:53 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "translucentroomname") == 0)
|
2020-01-25 05:43:04 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
graphics.translucentroomname = help.Int(pText);
|
2020-01-25 05:43:04 +01:00
|
|
|
}
|
|
|
|
|
2021-04-12 02:41:46 +02:00
|
|
|
if (SDL_strcmp(pKey, "musicvolume") == 0)
|
|
|
|
{
|
|
|
|
music.user_music_volume = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SDL_strcmp(pKey, "soundvolume") == 0)
|
|
|
|
{
|
|
|
|
music.user_sound_volume = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-04-19 08:23:44 +02:00
|
|
|
if (SDL_strcmp(pKey, "separate_interact") == 0)
|
|
|
|
{
|
|
|
|
separate_interact = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "flipButton") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
SDL_GameControllerButton newButton;
|
|
|
|
if (GetButtonFromString(pText, &newButton))
|
|
|
|
{
|
|
|
|
controllerButton_flip.push_back(newButton);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "enterButton") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
SDL_GameControllerButton newButton;
|
|
|
|
if (GetButtonFromString(pText, &newButton))
|
|
|
|
{
|
|
|
|
controllerButton_map.push_back(newButton);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "escButton") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
|
|
|
SDL_GameControllerButton newButton;
|
|
|
|
if (GetButtonFromString(pText, &newButton))
|
|
|
|
{
|
|
|
|
controllerButton_esc.push_back(newButton);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "restartButton") == 0)
|
2020-08-09 00:41:59 +02:00
|
|
|
{
|
|
|
|
SDL_GameControllerButton newButton;
|
|
|
|
if (GetButtonFromString(pText, &newButton))
|
|
|
|
{
|
|
|
|
controllerButton_restart.push_back(newButton);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-19 08:23:44 +02:00
|
|
|
if (SDL_strcmp(pKey, "interactButton") == 0)
|
|
|
|
{
|
|
|
|
SDL_GameControllerButton newButton;
|
|
|
|
if (GetButtonFromString(pText, &newButton))
|
|
|
|
{
|
|
|
|
controllerButton_interact.push_back(newButton);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "controllerSensitivity") == 0)
|
2020-04-02 22:01:55 +02:00
|
|
|
{
|
2020-11-13 02:16:18 +01:00
|
|
|
key.sensitivity = help.Int(pText);
|
2020-04-02 22:01:55 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
if (SDL_strcmp(pKey, "lang") == 0)
|
|
|
|
{
|
|
|
|
loc::lang = std::string(pText);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (SDL_strcmp(pKey, "lang_set") == 0)
|
|
|
|
{
|
|
|
|
loc::lang_set = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2023-10-02 02:44:13 +02:00
|
|
|
if (SDL_strcmp(pKey, "english_sprites") == 0)
|
|
|
|
{
|
|
|
|
loc::english_sprites = help.Int(pText);
|
|
|
|
}
|
|
|
|
|
2023-01-21 19:06:30 +01:00
|
|
|
if (SDL_strcmp(pKey, "new_level_font") == 0)
|
|
|
|
{
|
|
|
|
loc::new_level_font = std::string(pText);
|
|
|
|
}
|
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
if (SDL_strcmp(pKey, "roomname_translator") == 0 && loc::show_translator_menu)
|
|
|
|
{
|
|
|
|
roomname_translator::set_enabled(help.Int(pText));
|
|
|
|
}
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Fix regression with controller binds on first launch
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 3ef5248db93541f399e8220fe0280281881d8bd6 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.
2023-04-15 20:14:35 +02:00
|
|
|
setdefaultcontrollerbuttons();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-09-02 19:19:51 +02:00
|
|
|
bool Game::savestats(bool sync /*= true*/)
|
2020-12-22 01:34:16 +01:00
|
|
|
{
|
2021-12-25 09:18:51 +01:00
|
|
|
struct ScreenSettings screen_settings;
|
2021-12-25 09:22:22 +01:00
|
|
|
SDL_zero(screen_settings);
|
Extern `gameScreen`, remove `screenbuffer`
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.
2021-12-25 08:56:47 +01:00
|
|
|
gameScreen.GetSettings(&screen_settings);
|
2020-12-22 01:34:16 +01:00
|
|
|
|
2021-09-02 19:19:51 +02:00
|
|
|
return savestats(&screen_settings, sync);
|
2020-12-22 01:34:16 +01:00
|
|
|
}
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
bool Game::savestats(const struct ScreenSettings* screen_settings, bool sync /*= true*/)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-03 19:59:49 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
2022-03-14 06:39:09 +01:00
|
|
|
bool already_exists;
|
|
|
|
|
|
|
|
if (!stats_loaded)
|
|
|
|
{
|
|
|
|
vlog_warn("Stats not loaded! Not writing unlock.vvv.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
already_exists = FILESYSTEM_loadTiXml2Document("saves/unlock.vvv", doc);
|
2020-09-25 18:10:10 +02:00
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No unlock.vvv found. Creating new file");
|
2020-09-25 18:10:10 +02:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing unlock.vvv: %s", doc.ErrorStr());
|
|
|
|
vlog_info("Creating new unlock.vvv");
|
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
|
|
|
|
xml::update_declaration(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_comment(root, " Save file " );
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement * dataNode = xml::update_element(root, "Data");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string s_unlock;
|
2020-07-03 01:45:22 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(unlock); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_unlock += help.String(unlock[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "unlock", s_unlock.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string s_unlocknotify;
|
2020-07-03 01:45:22 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(unlocknotify); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_unlocknotify += help.String(unlocknotify[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "unlocknotify", s_unlocknotify.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string s_besttimes;
|
2020-07-03 02:09:30 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(besttimes); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_besttimes += help.String(besttimes[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "besttimes", s_besttimes.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-01 03:59:16 +02:00
|
|
|
std::string s_bestframes;
|
2020-07-03 02:09:30 +02:00
|
|
|
for (size_t i = 0; i < SDL_arraysize(bestframes); i++)
|
2020-07-01 03:59:16 +02:00
|
|
|
{
|
|
|
|
s_bestframes += help.String(bestframes[i]) + ",";
|
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "bestframes", s_bestframes.c_str());
|
2020-07-01 03:59:16 +02:00
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
std::string s_besttrinkets;
|
2020-07-03 02:09:30 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(besttrinkets); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_besttrinkets += help.String(besttrinkets[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "besttrinkets", s_besttrinkets.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string s_bestlives;
|
2020-07-03 02:09:30 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(bestlives); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_bestlives += help.String(bestlives[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "bestlives", s_bestlives.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string s_bestrank;
|
2020-07-03 02:09:30 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(bestrank); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
s_bestrank += help.String(bestrank[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "bestrank", s_bestrank.c_str());
|
|
|
|
|
|
|
|
xml::update_tag(dataNode, "bestgamedeaths", bestgamedeaths);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "stat_trinkets", stat_trinkets);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
xml::update_tag(dataNode, "swnbestrank", swnbestrank);
|
|
|
|
|
|
|
|
xml::update_tag(dataNode, "swnrecord", swnrecord);
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
serializesettings(dataNode, screen_settings);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
2021-09-02 19:19:51 +02:00
|
|
|
return FILESYSTEM_saveTiXml2Document("saves/unlock.vvv", doc, sync);
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
}
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::savestatsandsettings(void)
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
{
|
2021-09-02 19:19:51 +02:00
|
|
|
const bool stats_saved = savestats(false);
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
|
|
|
|
const bool settings_saved = savesettings();
|
2020-11-22 03:10:26 +01:00
|
|
|
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
return stats_saved && settings_saved; // Not the same as `savestats() && savesettings()`!
|
2020-11-22 03:10:26 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::savestatsandsettings_menu(void)
|
2020-11-22 03:10:26 +01:00
|
|
|
{
|
Refactor Game::savestats() to not use a default argument
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()
2020-12-22 01:03:19 +01:00
|
|
|
// Call Game::savestatsandsettings(), but upon failure, go to the save error screen
|
|
|
|
if (!savestatsandsettings() && !silence_settings_error)
|
2020-11-22 03:10:26 +01:00
|
|
|
{
|
|
|
|
createmenu(Menu::errorsavingsettings);
|
|
|
|
map.nexttowercolour();
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
void Game::serializesettings(tinyxml2::XMLElement* dataNode, const struct ScreenSettings* screen_settings)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
tinyxml2::XMLDocument& doc = xml::get_document(dataNode);
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "fullscreen", (int) screen_settings->fullscreen);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-12-23 04:54:59 +01:00
|
|
|
xml::update_tag(dataNode, "stretch", screen_settings->scalingMode);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "useLinearFilter", (int) screen_settings->linearFilter);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Store display index of window to settings
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.
2023-03-21 08:23:42 +01:00
|
|
|
xml::update_tag(dataNode, "window_display", screen_settings->windowDisplay);
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "window_width", screen_settings->windowWidth);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "window_height", screen_settings->windowHeight);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "noflashingmode", noflashingmode);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "colourblindmode", colourblindmode);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "setflipmode", graphics.setflipmode);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "invincibility", map.invincibility);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "slowdown", slowdown);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "advanced_smoothing", (int) screen_settings->badSignal);
|
2020-04-02 21:41:33 +02:00
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-11-13 01:48:39 +01:00
|
|
|
xml::update_tag(dataNode, "usingmmmmmm", music.usingmmmmmm);
|
Add a player trail to the editor (ghosts)
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)
2020-06-13 00:04:35 +02:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "ghostsenabled", (int) ghostsenabled);
|
2020-01-13 02:45:44 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "skipfakeload", (int) skipfakeload);
|
2020-06-30 04:49:14 +02:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "disablepause", (int) disablepause);
|
2020-01-17 18:37:53 +01:00
|
|
|
|
2021-08-05 21:20:05 +02:00
|
|
|
xml::update_tag(dataNode, "disableaudiopause", (int) disableaudiopause);
|
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "notextoutline", (int) graphics.notextoutline);
|
2020-01-25 05:43:04 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "translucentroomname", (int) graphics.translucentroomname);
|
2020-01-29 08:17:13 +01:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
xml::update_tag(dataNode, "over30mode", (int) over30mode);
|
|
|
|
|
2021-04-02 00:39:56 +02:00
|
|
|
xml::update_tag(dataNode, "inputdelay", (int) inputdelay);
|
|
|
|
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
xml::update_tag(
|
|
|
|
dataNode,
|
|
|
|
"glitchrunnermode",
|
|
|
|
GlitchrunnerMode_enum_to_string(GlitchrunnerMode_get())
|
|
|
|
);
|
2020-06-25 23:31:37 +02:00
|
|
|
|
2021-08-05 23:31:20 +02:00
|
|
|
xml::update_tag(dataNode, "showingametimer", (int) showingametimer);
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
xml::update_tag(dataNode, "vsync", (int) screen_settings->useVsync);
|
2020-09-25 18:10:10 +02:00
|
|
|
|
2021-04-12 02:41:46 +02:00
|
|
|
xml::update_tag(dataNode, "musicvolume", music.user_music_volume);
|
|
|
|
|
|
|
|
xml::update_tag(dataNode, "soundvolume", music.user_sound_volume);
|
|
|
|
|
2021-04-19 08:23:44 +02:00
|
|
|
xml::update_tag(dataNode, "separate_interact", (int) separate_interact);
|
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
// Delete all controller buttons we had previously.
|
|
|
|
// dataNode->FirstChildElement() shouldn't be NULL at this point...
|
|
|
|
// we've already added a bunch of elements
|
|
|
|
for (tinyxml2::XMLElement* element = dataNode->FirstChildElement();
|
|
|
|
element != NULL;
|
|
|
|
/* Increment code handled separately */)
|
|
|
|
{
|
|
|
|
const char* name = element->Name();
|
|
|
|
|
|
|
|
if (SDL_strcmp(name, "flipButton") == 0
|
|
|
|
|| SDL_strcmp(name, "enterButton") == 0
|
|
|
|
|| SDL_strcmp(name, "escButton") == 0
|
2021-04-19 08:23:44 +02:00
|
|
|
|| SDL_strcmp(name, "restartButton") == 0
|
|
|
|
|| SDL_strcmp(name, "interactButton") == 0)
|
2020-09-25 18:10:10 +02:00
|
|
|
{
|
|
|
|
// Can't just doc.DeleteNode(element) and then go to next,
|
|
|
|
// element->NextSiblingElement() will be NULL.
|
|
|
|
// Instead, store pointer of element we want to delete. Then
|
|
|
|
// increment `element`. And THEN delete the element.
|
|
|
|
tinyxml2::XMLElement* delete_this = element;
|
|
|
|
|
|
|
|
element = element->NextSiblingElement();
|
|
|
|
|
|
|
|
doc.DeleteNode(delete_this);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
element = element->NextSiblingElement();
|
|
|
|
}
|
2020-05-04 22:19:47 +02:00
|
|
|
|
2020-09-25 18:10:10 +02:00
|
|
|
// Now add them
|
2020-01-01 21:29:24 +01:00
|
|
|
for (size_t i = 0; i < controllerButton_flip.size(); i += 1)
|
|
|
|
{
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement* msg = doc.NewElement("flipButton");
|
2020-06-03 19:59:49 +02:00
|
|
|
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_flip[i]).c_str()));
|
2020-01-01 21:29:24 +01:00
|
|
|
dataNode->LinkEndChild(msg);
|
|
|
|
}
|
|
|
|
for (size_t i = 0; i < controllerButton_map.size(); i += 1)
|
|
|
|
{
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement* msg = doc.NewElement("enterButton");
|
2020-06-03 19:59:49 +02:00
|
|
|
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_map[i]).c_str()));
|
2020-01-01 21:29:24 +01:00
|
|
|
dataNode->LinkEndChild(msg);
|
|
|
|
}
|
|
|
|
for (size_t i = 0; i < controllerButton_esc.size(); i += 1)
|
|
|
|
{
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement* msg = doc.NewElement("escButton");
|
2020-06-03 19:59:49 +02:00
|
|
|
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_esc[i]).c_str()));
|
2020-01-01 21:29:24 +01:00
|
|
|
dataNode->LinkEndChild(msg);
|
|
|
|
}
|
2020-08-09 00:41:59 +02:00
|
|
|
for (size_t i = 0; i < controllerButton_restart.size(); i += 1)
|
|
|
|
{
|
2020-09-25 18:10:10 +02:00
|
|
|
tinyxml2::XMLElement* msg = doc.NewElement("restartButton");
|
2020-08-09 00:41:59 +02:00
|
|
|
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_restart[i]).c_str()));
|
|
|
|
dataNode->LinkEndChild(msg);
|
|
|
|
}
|
2021-04-19 08:23:44 +02:00
|
|
|
for (size_t i = 0; i < controllerButton_interact.size(); i += 1)
|
|
|
|
{
|
|
|
|
tinyxml2::XMLElement* msg = doc.NewElement("interactButton");
|
|
|
|
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_interact[i]).c_str()));
|
|
|
|
dataNode->LinkEndChild(msg);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-11-13 02:16:18 +01:00
|
|
|
xml::update_tag(dataNode, "controllerSensitivity", key.sensitivity);
|
2022-12-30 22:57:24 +01:00
|
|
|
|
|
|
|
xml::update_tag(dataNode, "lang", loc::lang.c_str());
|
|
|
|
xml::update_tag(dataNode, "lang_set", (int) loc::lang_set);
|
2023-10-02 02:44:13 +02:00
|
|
|
xml::update_tag(dataNode, "english_sprites", (int) loc::english_sprites);
|
2023-01-21 19:06:30 +01:00
|
|
|
xml::update_tag(dataNode, "new_level_font", loc::new_level_font.c_str());
|
2022-12-30 22:57:24 +01:00
|
|
|
xml::update_tag(dataNode, "roomname_translator", (int) roomname_translator::enabled);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2022-03-14 06:39:09 +01:00
|
|
|
static bool settings_loaded = false;
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
void Game::loadsettings(struct ScreenSettings* screen_settings)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
tinyxml2::XMLDocument doc;
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
tinyxml2::XMLElement* dataNode;
|
|
|
|
|
2022-03-14 06:39:09 +01:00
|
|
|
settings_loaded = true;
|
|
|
|
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
if (!FILESYSTEM_loadTiXml2Document("saves/settings.vvv", doc))
|
|
|
|
{
|
2020-12-22 01:40:46 +01:00
|
|
|
savesettings(screen_settings);
|
2021-03-24 02:35:38 +01:00
|
|
|
return;
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_error("Error parsing settings.vvv: %s", doc.ErrorStr());
|
|
|
|
return;
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
dataNode = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
2020-11-13 01:45:51 +01:00
|
|
|
deserializesettings(dataNode, screen_settings);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::savesettings(void)
|
2020-12-22 01:34:16 +01:00
|
|
|
{
|
2021-12-25 09:18:51 +01:00
|
|
|
struct ScreenSettings screen_settings;
|
2021-12-25 09:22:22 +01:00
|
|
|
SDL_zero(screen_settings);
|
Extern `gameScreen`, remove `screenbuffer`
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.
2021-12-25 08:56:47 +01:00
|
|
|
gameScreen.GetSettings(&screen_settings);
|
2020-12-22 01:34:16 +01:00
|
|
|
|
|
|
|
return savesettings(&screen_settings);
|
|
|
|
}
|
|
|
|
|
2021-12-25 09:18:51 +01:00
|
|
|
bool Game::savesettings(const struct ScreenSettings* screen_settings)
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
{
|
|
|
|
tinyxml2::XMLDocument doc;
|
2022-03-14 06:39:09 +01:00
|
|
|
bool already_exists;
|
|
|
|
|
|
|
|
if (!settings_loaded)
|
|
|
|
{
|
|
|
|
vlog_warn("Settings not loaded! Not writing settings.vvv.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
already_exists = FILESYSTEM_loadTiXml2Document("saves/settings.vvv", doc);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No settings.vvv found. Creating new file");
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing settings.vvv: %s", doc.ErrorStr());
|
|
|
|
vlog_info("Creating new settings.vvv");
|
|
|
|
}
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
|
|
|
xml::update_declaration(doc);
|
|
|
|
|
|
|
|
tinyxml2::XMLElement* root = xml::update_element(doc, "Settings");
|
|
|
|
|
|
|
|
xml::update_comment(root, " Settings (duplicated from unlock.vvv) ");
|
|
|
|
|
|
|
|
tinyxml2::XMLElement* dataNode = xml::update_element(root, "Data");
|
|
|
|
|
2020-12-22 01:34:16 +01:00
|
|
|
serializesettings(dataNode, screen_settings);
|
Move all settings to settings.vvv
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.
2020-11-04 08:11:21 +01:00
|
|
|
|
2020-11-22 03:10:26 +01:00
|
|
|
return FILESYSTEM_saveTiXml2Document("saves/settings.vvv", doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::customstart(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
jumpheld = true;
|
|
|
|
|
|
|
|
savex = edsavex;
|
|
|
|
savey = edsavey;
|
|
|
|
saverx = edsaverx;
|
|
|
|
savery = edsavery;
|
|
|
|
|
|
|
|
savegc = edsavegc;
|
|
|
|
savedir = edsavedir; //Worldmap Start
|
|
|
|
savepoint = 0;
|
|
|
|
gravitycontrol = savegc;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
deathseq = -1;
|
|
|
|
lifeseq = 0;
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::start(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
jumpheld = true;
|
|
|
|
|
|
|
|
savex = 232;
|
|
|
|
savey = 113;
|
|
|
|
saverx = 104;
|
|
|
|
savery = 110;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1; //Worldmap Start
|
|
|
|
savepoint = 0;
|
|
|
|
gravitycontrol = savegc;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
deathseq = -1;
|
|
|
|
lifeseq = 0;
|
|
|
|
|
2023-05-24 03:37:32 +02:00
|
|
|
if (!nocutscenes)
|
|
|
|
{
|
|
|
|
music.play(Music_PAUSE);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::deathsequence(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
int i;
|
|
|
|
if (supercrewmate && scmhurt)
|
|
|
|
{
|
|
|
|
i = obj.getscm();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
i = obj.getplayer();
|
|
|
|
}
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities))
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].colour = 1;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-06-13 05:36:08 +02:00
|
|
|
obj.entities[i].invis = false;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (deathseq == 30)
|
|
|
|
{
|
|
|
|
if (nodeathmode)
|
|
|
|
{
|
|
|
|
music.fadeout();
|
|
|
|
gameoverdelay = 60;
|
2023-05-22 08:27:33 +02:00
|
|
|
|
|
|
|
/* Fix a bug being able to play music on the Game Over screen */
|
|
|
|
music.nicefade = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
deathcounts++;
|
2023-06-04 00:29:02 +02:00
|
|
|
music.playef(Sound_CRY);
|
2023-04-06 01:45:40 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && !noflashingmode)
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (map.finalmode)
|
|
|
|
{
|
2020-05-20 02:20:46 +02:00
|
|
|
if (roomx - 41 >= 0 && roomx - 41 < 20 && roomy - 48 >= 0 && roomy - 48 < 20)
|
|
|
|
{
|
|
|
|
map.roomdeathsfinal[roomx - 41 + (20 * (roomy - 48))]++;
|
|
|
|
currentroomdeaths = map.roomdeathsfinal[roomx - 41 + (20 * (roomy - 48))];
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-05-20 02:20:46 +02:00
|
|
|
if (roomx - 100 >= 0 && roomx - 100 < 20 && roomy - 100 >= 0 && roomy - 100 < 20)
|
|
|
|
{
|
|
|
|
map.roomdeaths[roomx - 100 + (20*(roomy - 100))]++;
|
|
|
|
currentroomdeaths = map.roomdeaths[roomx - 100 + (20 * (roomy - 100))];
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
2023-04-06 01:45:40 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && !noflashingmode)
|
2020-06-13 05:36:08 +02:00
|
|
|
{
|
|
|
|
if (deathseq == 25) obj.entities[i].invis = true;
|
|
|
|
if (deathseq == 20) obj.entities[i].invis = true;
|
|
|
|
if (deathseq == 16) obj.entities[i].invis = true;
|
|
|
|
if (deathseq == 14) obj.entities[i].invis = true;
|
|
|
|
if (deathseq == 12) obj.entities[i].invis = true;
|
|
|
|
if (deathseq < 10) obj.entities[i].invis = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (!nodeathmode)
|
|
|
|
{
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(i, obj.entities) && deathseq <= 1) obj.entities[i].invis = false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
gameoverdelay--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
void Game::startspecial( int t )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
jumpheld = true;
|
|
|
|
|
|
|
|
switch(t)
|
|
|
|
{
|
|
|
|
case 0: //Secret Lab
|
|
|
|
savex = 104;
|
|
|
|
savey = 169;
|
|
|
|
saverx = 118;
|
|
|
|
savery = 106;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
case 1: //Intermission 1 (any)
|
|
|
|
savex = 80;
|
|
|
|
savey = 57;
|
|
|
|
saverx = 41;
|
|
|
|
savery = 56;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 0;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
savex = 232;
|
|
|
|
savey = 113;
|
|
|
|
saverx = 104;
|
|
|
|
savery = 110;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1; //Worldmap Start
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
savepoint = 0;
|
|
|
|
gravitycontrol = savegc;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
deathseq = -1;
|
|
|
|
lifeseq = 0;
|
|
|
|
}
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
void Game::starttrial( int t )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
jumpheld = true;
|
|
|
|
|
|
|
|
switch(t)
|
|
|
|
{
|
|
|
|
case 0: //Space Station 1
|
|
|
|
savex = 200;
|
|
|
|
savey = 161;
|
|
|
|
saverx = 113;
|
|
|
|
savery = 105;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
case 1: //Lab
|
|
|
|
savex = 191;
|
|
|
|
savey = 33;
|
|
|
|
saverx = 102;
|
|
|
|
savery = 116;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
case 2: //Tower
|
|
|
|
savex = 84;
|
|
|
|
savey = 193, saverx = 108;
|
|
|
|
savery = 109;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
case 3: //Space Station 2
|
|
|
|
savex = 148;
|
|
|
|
savey = 38;
|
|
|
|
saverx = 112;
|
|
|
|
savery = 114;
|
|
|
|
savegc = 1;
|
|
|
|
savedir = 0;
|
|
|
|
break;
|
|
|
|
case 4: //Warp
|
|
|
|
savex = 52;
|
|
|
|
savey = 73;
|
|
|
|
saverx = 114;
|
|
|
|
savery = 101;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
case 5: //Final
|
|
|
|
savex = 101;
|
|
|
|
savey = 113;
|
|
|
|
saverx = 46;
|
|
|
|
savery = 54;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
savex = 232;
|
|
|
|
savey = 113;
|
|
|
|
saverx = 104;
|
|
|
|
savery = 110;
|
|
|
|
savegc = 0;
|
|
|
|
savedir = 1; //Worldmap Start
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
savepoint = 0;
|
|
|
|
gravitycontrol = savegc;
|
|
|
|
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-01-01 21:29:24 +01:00
|
|
|
deathseq = -1;
|
|
|
|
lifeseq = 0;
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::loadquick(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-04 02:24:31 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
if (!FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc)) return;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
readmaingamesave("qsave.vvv", doc);
|
2020-09-14 01:31:02 +02:00
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
void Game::readmaingamesave(const char* savename, tinyxml2::XMLDocument& doc)
|
2020-09-14 01:31:02 +02:00
|
|
|
{
|
2020-06-04 02:24:31 +02:00
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
tinyxml2::XMLElement* pElem;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_error("Error parsing %s: %s", savename, doc.ErrorStr());
|
|
|
|
return;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2023-09-04 15:27:29 +02:00
|
|
|
/* Even if we want the default hardest room to be Welcome Aboard, there are pre-2.4
|
|
|
|
* saves with JUST <hardestroom> which should take priority over the coords */
|
|
|
|
hardestroom_x = -1;
|
|
|
|
hardestroom_y = -1;
|
|
|
|
hardestroom_specialname = false;
|
|
|
|
hardestroom_finalstretch = false;
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
|
|
|
pElem != NULL;
|
|
|
|
pElem = pElem->NextSiblingElement())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
const char* pKey = pElem->Value();
|
|
|
|
const char* pText = pElem->GetText();
|
2020-01-01 21:29:24 +01:00
|
|
|
if(pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
|
|
|
|
2020-07-03 06:01:09 +02:00
|
|
|
LOAD_ARRAY_RENAME(worldmap, map.explored)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 03:22:19 +02:00
|
|
|
LOAD_ARRAY_RENAME(flags, obj.flags)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 03:10:52 +02:00
|
|
|
LOAD_ARRAY(crewstats)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 04:17:32 +02:00
|
|
|
LOAD_ARRAY_RENAME(collect, obj.collect)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "finalmode") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.finalmode = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "finalstretch") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.finalstretch = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "savex") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savex = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savey") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savey = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "saverx") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
saverx = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savery") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savery = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savegc") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savegc = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savedir") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savedir= help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savepoint") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savepoint = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "companion") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
companion = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "lastsaved") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
lastsaved = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "teleportscript") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
teleportscript = pText;
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "supercrewmate") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
supercrewmate = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "scmprogress") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
scmprogress = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "frames") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
frames = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "seconds") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
seconds = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "minutes") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
minutes = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hours") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
hours = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "deathcounts") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
deathcounts = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "totalflips") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
totalflips = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroom") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-05-18 23:16:23 +02:00
|
|
|
hardestroom = pText;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroomdeaths") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
hardestroomdeaths = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-09-04 15:27:29 +02:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_x") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_x = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_y") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_y = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_specialname") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_specialname = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_finalstretch") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_finalstretch = help.Int(pText);
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "currentsong") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
int song = help.Int(pText);
|
Fix silent music saves playing MMMMMM track 15 if...
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.
2020-08-07 03:36:35 +02:00
|
|
|
if (song != -1)
|
|
|
|
{
|
|
|
|
music.play(song);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
Save showtargets to main game save files
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.
2021-05-20 22:44:06 +02:00
|
|
|
else if (SDL_strcmp(pKey, "showtargets") == 0)
|
|
|
|
{
|
|
|
|
map.showtargets = help.Int(pText);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (map.finalmode)
|
|
|
|
{
|
|
|
|
map.final_colormode = false;
|
|
|
|
map.final_mapcol = 0;
|
|
|
|
map.final_colorframe = 0;
|
|
|
|
}
|
|
|
|
if (map.finalstretch)
|
|
|
|
{
|
|
|
|
map.finalstretch = true;
|
|
|
|
map.final_colormode = true;
|
|
|
|
map.final_mapcol = 0;
|
|
|
|
map.final_colorframe = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
map.showteleporters = true;
|
2020-04-09 08:34:26 +02:00
|
|
|
if(obj.flags[12]) map.showtargets = true;
|
|
|
|
if (obj.flags[42]) map.showtrinkets = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-09-07 00:41:49 +02:00
|
|
|
void Game::customloadquick(const std::string& savfile)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
std::string levelfile;
|
|
|
|
|
|
|
|
if (cliplaytest)
|
|
|
|
{
|
2020-04-09 21:03:24 +02:00
|
|
|
savex = playx;
|
2020-05-04 20:41:38 +02:00
|
|
|
savey = playy;
|
2020-04-09 21:03:24 +02:00
|
|
|
saverx = playrx;
|
|
|
|
savery = playry;
|
|
|
|
savegc = playgc;
|
CLI: Allow no music to be played if save pos used
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.
2023-06-10 20:41:32 +02:00
|
|
|
if (playmusic > -1)
|
|
|
|
{
|
|
|
|
music.play(playmusic);
|
|
|
|
}
|
2020-04-09 21:03:24 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
levelfile = savfile.substr(7);
|
|
|
|
if (!FILESYSTEM_loadTiXml2Document(("saves/"+levelfile+".vvv").c_str(), doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_error("%s.vvv not found", levelfile.c_str());
|
|
|
|
return;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing %s.vvv: %s", levelfile.c_str(), doc.ErrorStr());
|
|
|
|
return;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2023-09-04 15:27:29 +02:00
|
|
|
// Like readmaingamesave(...), old saves have just <hardestroom>
|
|
|
|
hardestroom_x = -1;
|
|
|
|
hardestroom_y = -1;
|
|
|
|
hardestroom_specialname = false;
|
|
|
|
hardestroom_finalstretch = false;
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
|
|
|
pElem != NULL;
|
|
|
|
pElem = pElem->NextSiblingElement())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-03-06 19:31:17 +01:00
|
|
|
const char* pKey = pElem->Value();
|
2020-01-01 21:29:24 +01:00
|
|
|
const char* pText = pElem->GetText() ;
|
|
|
|
if(pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
|
|
|
|
2020-07-03 06:01:09 +02:00
|
|
|
LOAD_ARRAY_RENAME(worldmap, map.explored)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 03:22:19 +02:00
|
|
|
LOAD_ARRAY_RENAME(flags, obj.flags)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 03:10:52 +02:00
|
|
|
LOAD_ARRAY_RENAME(moods, obj.customcrewmoods)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 03:10:52 +02:00
|
|
|
LOAD_ARRAY(crewstats)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 04:17:32 +02:00
|
|
|
LOAD_ARRAY_RENAME(collect, obj.collect)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-07-03 04:17:32 +02:00
|
|
|
LOAD_ARRAY_RENAME(customcollect, obj.customcollect)
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "finalmode") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.finalmode = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "finalstretch") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.finalstretch = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (map.finalmode)
|
|
|
|
{
|
|
|
|
map.final_colormode = false;
|
|
|
|
map.final_mapcol = 0;
|
|
|
|
map.final_colorframe = 0;
|
|
|
|
}
|
|
|
|
if (map.finalstretch)
|
|
|
|
{
|
|
|
|
map.finalstretch = true;
|
|
|
|
map.final_colormode = true;
|
|
|
|
map.final_mapcol = 0;
|
|
|
|
map.final_colorframe = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-03-06 19:31:17 +01:00
|
|
|
if (SDL_strcmp(pKey, "savex") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savex = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savey") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savey = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "saverx") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
saverx = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savery") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savery = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savegc") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savegc = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savedir") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savedir= help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "savepoint") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
savepoint = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-09-07 03:28:28 +02:00
|
|
|
else if (SDL_strcmp(pKey, "savecolour") == 0)
|
|
|
|
{
|
|
|
|
savecolour = help.Int(pText);
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "companion") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
companion = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "lastsaved") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
lastsaved = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "teleportscript") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
teleportscript = pText;
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "supercrewmate") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
supercrewmate = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "scmprogress") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
scmprogress = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "frames") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
frames = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "seconds") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
seconds = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "minutes") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
minutes = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hours") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
hours = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "deathcounts") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
deathcounts = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "totalflips") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
totalflips = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroom") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-05-18 23:16:23 +02:00
|
|
|
hardestroom = pText;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroomdeaths") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
hardestroomdeaths = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-09-04 15:27:29 +02:00
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_x") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_x = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_y") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_y = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_specialname") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_specialname = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hardestroom_finalstretch") == 0)
|
|
|
|
{
|
|
|
|
hardestroom_finalstretch = help.Int(pText);
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "currentsong") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
int song = help.Int(pText);
|
Fix silent music saves playing MMMMMM track 15 if...
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.
2020-08-07 03:36:35 +02:00
|
|
|
if (song != -1)
|
|
|
|
{
|
|
|
|
music.play(song);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2022-12-30 22:57:24 +01:00
|
|
|
else if (SDL_strcmp(pKey, "lang_custom") == 0)
|
|
|
|
{
|
|
|
|
loc::lang_custom = pText;
|
|
|
|
if (pText[0] != '\0')
|
|
|
|
{
|
|
|
|
loc::loadtext_custom(NULL);
|
|
|
|
}
|
|
|
|
}
|
2021-03-06 19:31:17 +01:00
|
|
|
else if (SDL_strcmp(pKey, "showminimap") == 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-08-07 06:31:29 +02:00
|
|
|
map.customshowmm = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-08-12 03:08:32 +02:00
|
|
|
else if (SDL_strcmp(pKey, "disabletemporaryaudiopause") == 0)
|
|
|
|
{
|
|
|
|
disabletemporaryaudiopause = help.Int(pText);
|
|
|
|
}
|
2021-06-14 02:24:16 +02:00
|
|
|
else if (SDL_strcmp(pKey, "showtrinkets") == 0)
|
|
|
|
{
|
|
|
|
map.showtrinkets = help.Int(pText);
|
|
|
|
}
|
2022-12-12 00:05:20 +01:00
|
|
|
else if (SDL_strcmp(pKey, "roomname") == 0)
|
|
|
|
{
|
|
|
|
map.setroomname(pText);
|
|
|
|
map.roomnameset = true;
|
|
|
|
map.roomname_special = true;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 00:43:02 +01:00
|
|
|
static void loadthissummary(
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
const char* filename,
|
2023-09-12 03:04:34 +02:00
|
|
|
struct Game::Summary* summary,
|
2021-03-24 00:43:02 +01:00
|
|
|
tinyxml2::XMLDocument& doc
|
|
|
|
) {
|
|
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
if (doc.Error())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
vlog_error("Error parsing %s: %s", filename, doc.ErrorStr());
|
|
|
|
return;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
summary->exists = true;
|
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
for (pElem = hDoc
|
|
|
|
.FirstChildElement()
|
|
|
|
.FirstChildElement("Data")
|
|
|
|
.FirstChildElement()
|
|
|
|
.ToElement();
|
|
|
|
pElem != NULL;
|
|
|
|
pElem = pElem->NextSiblingElement())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-03-24 00:43:02 +01:00
|
|
|
const char* pKey = pElem->Value();
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
const char* pText = pElem->GetText();
|
|
|
|
|
|
|
|
if (pText == NULL)
|
|
|
|
{
|
|
|
|
pText = "";
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (SDL_strcmp(pKey, "seconds") == 0)
|
2021-03-24 00:43:02 +01:00
|
|
|
{
|
|
|
|
summary->seconds = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "minutes") == 0)
|
|
|
|
{
|
|
|
|
summary->minutes = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "hours") == 0)
|
|
|
|
{
|
|
|
|
summary->hours = help.Int(pText);
|
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "saverx") == 0)
|
|
|
|
{
|
2023-09-12 03:22:18 +02:00
|
|
|
summary->saverx = help.Int(pText);
|
2021-03-24 00:43:02 +01:00
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "savery") == 0)
|
|
|
|
{
|
2023-09-12 03:22:18 +02:00
|
|
|
summary->savery = help.Int(pText);
|
2021-03-24 00:43:02 +01:00
|
|
|
}
|
|
|
|
else if (SDL_strcmp(pKey, "trinkets") == 0)
|
|
|
|
{
|
|
|
|
summary->trinkets = help.Int(pText);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-03-24 00:43:02 +01:00
|
|
|
LOAD_ARRAY_RENAME(crewstats, summary->crewstats)
|
|
|
|
}
|
|
|
|
}
|
2021-03-24 20:19:44 +01:00
|
|
|
|
2021-03-24 00:43:02 +01:00
|
|
|
void Game::loadsummary(void)
|
|
|
|
{
|
|
|
|
tinyxml2::XMLDocument doc;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
SDL_zero(last_telesave);
|
|
|
|
SDL_zero(last_quicksave);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc))
|
|
|
|
{
|
|
|
|
loadthissummary("tsave.vvv", &last_telesave, doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
loadthissummary("qsave.vvv", &last_quicksave, doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::initteleportermode(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//Set the teleporter variable to the right position!
|
|
|
|
teleport_to_teleporter = 0;
|
|
|
|
|
2020-04-15 04:32:30 +02:00
|
|
|
for (size_t i = 0; i < map.teleporters.size(); i++)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
if (roomx == map.teleporters[i].x + 100 && roomy == map.teleporters[i].y + 100)
|
|
|
|
{
|
|
|
|
teleport_to_teleporter = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::savetele(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-14 05:08:39 +02:00
|
|
|
if (map.custommode || inspecial())
|
2020-03-15 16:17:12 +01:00
|
|
|
{
|
|
|
|
//Don't trash save data!
|
2020-11-04 02:58:38 +01:00
|
|
|
return false;
|
2020-03-15 16:17:12 +01:00
|
|
|
}
|
|
|
|
|
2020-06-03 20:39:33 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
2020-09-25 18:10:48 +02:00
|
|
|
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc);
|
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No tsave.vvv found. Creating new file");
|
2020-09-25 18:10:48 +02:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing tsave.vvv: %s", doc.ErrorStr());
|
|
|
|
vlog_info("Creating new tsave.vvv");
|
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
last_telesave = writemaingamesave(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-11-04 02:58:38 +01:00
|
|
|
if(!FILESYSTEM_saveTiXml2Document("saves/tsave.vvv", doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Could Not Save game!");
|
|
|
|
vlog_error("Failed: %s%s", saveFilePath, "tsave.vvv");
|
2020-11-04 02:58:38 +01:00
|
|
|
return false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("Game saved");
|
2020-11-04 02:58:38 +01:00
|
|
|
return true;
|
2020-09-14 01:31:02 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::savequick(void)
|
2020-09-14 01:31:02 +02:00
|
|
|
{
|
|
|
|
if (map.custommode || inspecial())
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-09-14 01:31:02 +02:00
|
|
|
//Don't trash save data!
|
2020-11-04 03:45:33 +01:00
|
|
|
return false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2020-09-14 01:31:02 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
2020-09-25 18:10:48 +02:00
|
|
|
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc);
|
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No qsave.vvv found. Creating new file");
|
2020-09-25 18:10:48 +02:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing qsave.vvv: %s", doc.ErrorStr());
|
|
|
|
vlog_info("Creating new qsave.vvv");
|
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
last_quicksave = writemaingamesave(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-11-04 03:45:33 +01:00
|
|
|
if(!FILESYSTEM_saveTiXml2Document("saves/qsave.vvv", doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Could Not Save game!");
|
|
|
|
vlog_error("Failed: %s%s", saveFilePath, "qsave.vvv");
|
2020-11-04 03:45:33 +01:00
|
|
|
return false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("Game saved");
|
2020-11-04 03:45:33 +01:00
|
|
|
return true;
|
2020-09-14 01:31:02 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-14 01:31:02 +02:00
|
|
|
// Returns summary of save
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
struct Game::Summary Game::writemaingamesave(tinyxml2::XMLDocument& doc)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-09-14 01:31:02 +02:00
|
|
|
//TODO make this code a bit cleaner.
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
struct Game::Summary summary;
|
|
|
|
SDL_zero(summary);
|
|
|
|
|
2020-06-14 05:08:39 +02:00
|
|
|
if (map.custommode || inspecial())
|
2020-03-15 16:17:12 +01:00
|
|
|
{
|
|
|
|
//Don't trash save data!
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
return summary;
|
2020-03-15 16:17:12 +01:00
|
|
|
}
|
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_declaration(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_comment(root, " Save file " );
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
//Flags, map and stats
|
|
|
|
|
|
|
|
std::string mapExplored;
|
2020-07-03 06:01:09 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(map.explored); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
mapExplored += help.String(map.explored[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "worldmap", mapExplored.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string flags;
|
2020-07-03 03:22:19 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.flags); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
flags += help.String((int) obj.flags[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "flags", flags.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string crewstatsString;
|
2020-07-03 03:10:52 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(crewstats); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
crewstatsString += help.String(crewstats[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "crewstats", crewstatsString.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string collect;
|
2020-07-03 04:17:32 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.collect); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:56:36 +02:00
|
|
|
collect += help.String((int) obj.collect[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "collect", collect.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
//Position
|
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savex", savex);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savey", savey);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "saverx", saverx);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savery", savery);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savegc", savegc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savedir", savedir);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "savepoint", savepoint);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
int n_trinkets = trinkets();
|
|
|
|
xml::update_tag(msgs, "trinkets", n_trinkets);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
//Special stats
|
2020-04-03 00:11:45 +02:00
|
|
|
|
2020-11-06 09:20:58 +01:00
|
|
|
if (music.nicefade)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "currentsong", music.nicechange);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "currentsong", music.currentsong);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Save showtargets to main game save files
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.
2021-05-20 22:44:06 +02:00
|
|
|
xml::update_tag(msgs, "showtargets", (int) map.showtargets);
|
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "teleportscript", teleportscript.c_str());
|
|
|
|
xml::update_tag(msgs, "companion", companion);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "lastsaved", lastsaved);
|
2020-09-25 18:11:52 +02:00
|
|
|
xml::update_tag(msgs, "supercrewmate", (int) supercrewmate);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "scmprogress", scmprogress);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "frames", frames);
|
|
|
|
xml::update_tag(msgs, "seconds", seconds);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "minutes", minutes);
|
|
|
|
xml::update_tag(msgs, "hours", hours);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "deathcounts", deathcounts);
|
|
|
|
xml::update_tag(msgs, "totalflips", totalflips);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:10:48 +02:00
|
|
|
xml::update_tag(msgs, "hardestroom", hardestroom.c_str());
|
|
|
|
xml::update_tag(msgs, "hardestroomdeaths", hardestroomdeaths);
|
2023-09-04 15:27:29 +02:00
|
|
|
xml::update_tag(msgs, "hardestroom_x", hardestroom_x);
|
|
|
|
xml::update_tag(msgs, "hardestroom_y", hardestroom_y);
|
|
|
|
xml::update_tag(msgs, "hardestroom_specialname", (int) hardestroom_specialname);
|
|
|
|
xml::update_tag(msgs, "hardestroom_finalstretch", (int) hardestroom_finalstretch);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:52 +02:00
|
|
|
xml::update_tag(msgs, "finalmode", (int) map.finalmode);
|
|
|
|
xml::update_tag(msgs, "finalstretch", (int) map.finalstretch);
|
2020-09-14 01:31:02 +02:00
|
|
|
|
|
|
|
|
2023-09-12 04:15:12 +02:00
|
|
|
std::string legacy_summary = std::string(map.currentarea(saverx, savery)) + ", " + timestring();
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
xml::update_tag(msgs, "summary", legacy_summary.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
summary.exists = true;
|
|
|
|
summary.seconds = seconds;
|
|
|
|
summary.minutes = minutes;
|
|
|
|
summary.hours = hours;
|
|
|
|
summary.saverx = saverx;
|
|
|
|
summary.savery = savery;
|
|
|
|
summary.trinkets = n_trinkets;
|
|
|
|
SDL_memcpy(summary.crewstats, crewstats, sizeof(summary.crewstats));
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-14 01:31:02 +02:00
|
|
|
return summary;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2020-09-14 01:31:02 +02:00
|
|
|
|
2021-09-07 00:41:49 +02:00
|
|
|
bool Game::customsavequick(const std::string& savfile)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-09-25 18:11:03 +02:00
|
|
|
const std::string levelfile = savfile.substr(7);
|
|
|
|
|
2020-06-04 02:36:27 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
2020-09-25 18:11:03 +02:00
|
|
|
bool already_exists = FILESYSTEM_loadTiXml2Document(("saves/" + levelfile + ".vvv").c_str(), doc);
|
|
|
|
if (!already_exists)
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("No %s.vvv found. Creating new file", levelfile.c_str());
|
2020-09-25 18:11:03 +02:00
|
|
|
}
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
else if (doc.Error())
|
|
|
|
{
|
|
|
|
vlog_error("Error parsing existing %s.vvv: %s", levelfile.c_str(), doc.ErrorStr());
|
|
|
|
vlog_info("Creating new %s.vvv", levelfile.c_str());
|
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
|
|
|
|
xml::update_declaration(doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_comment(root, " Save file ");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
//Flags, map and stats
|
|
|
|
|
|
|
|
std::string mapExplored;
|
2020-07-03 06:01:09 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(map.explored); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
mapExplored += help.String(map.explored[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "worldmap", mapExplored.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string flags;
|
2020-07-03 03:22:19 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.flags); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:34:26 +02:00
|
|
|
flags += help.String((int) obj.flags[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "flags", flags.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string moods;
|
2020-07-03 03:10:52 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.customcrewmoods); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
moods += help.String(obj.customcrewmoods[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "moods", moods.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string crewstatsString;
|
2020-07-03 03:10:52 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(crewstats); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-02 21:30:37 +02:00
|
|
|
crewstatsString += help.String(crewstats[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "crewstats", crewstatsString.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string collect;
|
2020-07-03 04:17:32 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.collect); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:56:36 +02:00
|
|
|
collect += help.String((int) obj.collect[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "collect", collect.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
std::string customcollect;
|
2020-07-03 04:17:32 +02:00
|
|
|
for(size_t i = 0; i < SDL_arraysize(obj.customcollect); i++ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-09 08:58:03 +02:00
|
|
|
customcollect += help.String((int) obj.customcollect[i]) + ",";
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "customcollect", customcollect.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
//Position
|
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savex", savex);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savey", savey);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "saverx", saverx);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savery", savery);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savegc", savegc);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savedir", savedir);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "savepoint", savepoint);
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2021-09-07 03:28:28 +02:00
|
|
|
xml::update_tag(msgs, "savecolour", savecolour);
|
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "trinkets", trinkets());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "crewmates", crewmates());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
//Special stats
|
2020-04-03 00:11:45 +02:00
|
|
|
|
2020-11-06 09:20:58 +01:00
|
|
|
if (music.nicefade)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "currentsong", music.nicechange );
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "currentsong", music.currentsong);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
xml::update_tag(msgs, "lang_custom", loc::lang_custom.c_str());
|
|
|
|
|
2020-09-25 18:11:03 +02:00
|
|
|
xml::update_tag(msgs, "teleportscript", teleportscript.c_str());
|
|
|
|
xml::update_tag(msgs, "companion", companion);
|
|
|
|
|
|
|
|
xml::update_tag(msgs, "lastsaved", lastsaved);
|
2020-09-25 18:11:52 +02:00
|
|
|
xml::update_tag(msgs, "supercrewmate", (int) supercrewmate);
|
2020-09-25 18:11:03 +02:00
|
|
|
|
|
|
|
xml::update_tag(msgs, "scmprogress", scmprogress);
|
|
|
|
|
|
|
|
|
|
|
|
xml::update_tag(msgs, "frames", frames);
|
|
|
|
xml::update_tag(msgs, "seconds", seconds);
|
|
|
|
|
|
|
|
xml::update_tag(msgs, "minutes", minutes);
|
|
|
|
xml::update_tag(msgs, "hours", hours);
|
|
|
|
|
|
|
|
xml::update_tag(msgs, "deathcounts", deathcounts);
|
|
|
|
xml::update_tag(msgs, "totalflips", totalflips);
|
|
|
|
|
|
|
|
xml::update_tag(msgs, "hardestroom", hardestroom.c_str());
|
|
|
|
xml::update_tag(msgs, "hardestroomdeaths", hardestroomdeaths);
|
2023-09-04 15:27:29 +02:00
|
|
|
xml::update_tag(msgs, "hardestroom_x", hardestroom_x);
|
|
|
|
xml::update_tag(msgs, "hardestroom_y", hardestroom_y);
|
|
|
|
xml::update_tag(msgs, "hardestroom_specialname", (int) hardestroom_specialname);
|
|
|
|
xml::update_tag(msgs, "hardestroom_finalstretch", (int) hardestroom_finalstretch);
|
2020-09-25 18:11:03 +02:00
|
|
|
|
2020-09-25 18:11:52 +02:00
|
|
|
xml::update_tag(msgs, "showminimap", (int) map.customshowmm);
|
2020-09-25 18:11:03 +02:00
|
|
|
|
2021-08-12 03:08:32 +02:00
|
|
|
xml::update_tag(msgs, "disabletemporaryaudiopause", (int) disabletemporaryaudiopause);
|
|
|
|
|
2021-06-14 02:24:16 +02:00
|
|
|
xml::update_tag(msgs, "showtrinkets", (int) map.showtrinkets);
|
|
|
|
|
2022-12-12 00:05:20 +01:00
|
|
|
if (map.roomnameset)
|
|
|
|
{
|
|
|
|
xml::update_tag(msgs, "roomname", map.roomname);
|
|
|
|
}
|
2023-02-18 00:52:57 +01:00
|
|
|
else
|
|
|
|
{
|
|
|
|
// If there's roomname tags, remove them. There will probably only always be one, but just in case...
|
|
|
|
tinyxml2::XMLElement* element;
|
|
|
|
while ((element = msgs->FirstChildElement("roomname")) != NULL)
|
|
|
|
{
|
|
|
|
doc.DeleteNode(element);
|
|
|
|
}
|
|
|
|
}
|
2022-12-12 00:05:20 +01:00
|
|
|
|
2023-09-12 04:15:12 +02:00
|
|
|
std::string legacy_summary = customleveltitle + ", " + timestring();
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
xml::update_tag(msgs, "summary", legacy_summary.c_str());
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-11-04 03:45:33 +01:00
|
|
|
if(!FILESYSTEM_saveTiXml2Document(("saves/"+levelfile+".vvv").c_str(), doc))
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Could Not Save game!");
|
|
|
|
vlog_error("Failed: %s%s%s", saveFilePath, levelfile.c_str(), ".vvv");
|
2020-11-04 03:45:33 +01:00
|
|
|
return false;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_info("Game saved");
|
2020-11-04 03:45:33 +01:00
|
|
|
return true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-03-19 05:20:05 +01:00
|
|
|
void Game::loadtele(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-06-03 20:04:36 +02:00
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
if (!FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc)) return;
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Simplify and print XML errors from TinyXML-2
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.
2021-03-24 02:44:16 +01:00
|
|
|
readmaingamesave("tsave.vvv", doc);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
std::string Game::unrescued(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//Randomly return the name of an unrescued crewmate
|
2022-12-30 22:57:24 +01:00
|
|
|
//Localization is handled with regular cutscene dialogue
|
2020-01-01 21:29:24 +01:00
|
|
|
if (fRandom() * 100 > 50)
|
|
|
|
{
|
|
|
|
if (!crewstats[5]) return "Victoria";
|
|
|
|
if (!crewstats[2]) return "Vitellary";
|
|
|
|
if (!crewstats[4]) return "Verdigris";
|
|
|
|
if (!crewstats[3]) return "Vermilion";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (fRandom() * 100 > 50)
|
|
|
|
{
|
|
|
|
if (!crewstats[2]) return "Vitellary";
|
|
|
|
if (!crewstats[4]) return "Verdigris";
|
|
|
|
if (!crewstats[3]) return "Vermilion";
|
|
|
|
if (!crewstats[5]) return "Victoria";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (!crewstats[4]) return "Verdigris";
|
|
|
|
if (!crewstats[3]) return "Vermilion";
|
|
|
|
if (!crewstats[5]) return "Victoria";
|
|
|
|
if (!crewstats[2]) return "Vitellary";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "you";
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::gameclock(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-04-13 23:38:45 +02:00
|
|
|
if (timetrialcountdown > 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-02 22:01:55 +02:00
|
|
|
frames++;
|
|
|
|
if (frames >= 30)
|
|
|
|
{
|
|
|
|
frames -= 30;
|
|
|
|
seconds++;
|
|
|
|
if (seconds >= 60)
|
|
|
|
{
|
|
|
|
seconds -= 60;
|
|
|
|
minutes++;
|
|
|
|
if (minutes >= 60)
|
|
|
|
{
|
|
|
|
minutes -= 60;
|
|
|
|
hours++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
std::string Game::giventimestring( int hrs, int min, int sec )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
return timetstring(help.hms_to_seconds(hrs, min, sec));
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
std::string Game::timestring(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
return giventimestring(hours, minutes, seconds);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
std::string Game::resulttimestring(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//given result time in seconds:
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
char output[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
help.format_time(output, sizeof(output), timetrialresulttime, timetrialresultframes, true);
|
|
|
|
return output;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2020-03-31 02:16:02 +02:00
|
|
|
std::string Game::timetstring( int t )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//given par time in seconds:
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
char output[SCREEN_WIDTH_CHARS + 1];
|
|
|
|
help.format_time(output, sizeof(output), t, -1, true);
|
|
|
|
return output;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-12-21 02:32:12 +01:00
|
|
|
void Game::timestringcenti(char* buffer, const size_t buffer_size)
|
|
|
|
{
|
Simplify time formatting functions
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)
2021-12-25 17:13:46 +01:00
|
|
|
help.format_time(buffer, buffer_size, help.hms_to_seconds(hours, minutes, seconds), frames, true);
|
2021-12-21 02:32:12 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::returnmenu(void)
|
2020-04-17 04:16:40 +02:00
|
|
|
{
|
|
|
|
if (menustack.empty())
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error: returning to previous menu frame on empty stack!");
|
2020-04-17 04:16:40 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Kludge-fix being able to play music in editor
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.
2021-09-11 03:56:12 +02:00
|
|
|
/* FIXME: Super bad kludge, don't hardcode this! */
|
|
|
|
if (currentmenuname == Menu::ed_music)
|
|
|
|
{
|
|
|
|
music.fadeout();
|
|
|
|
}
|
2024-01-10 09:19:16 +01:00
|
|
|
else if (currentmenuname == Menu::gamecompletecontinue
|
|
|
|
|| currentmenuname == Menu::timetrialcomplete3
|
|
|
|
|| currentmenuname == Menu::gameover2
|
|
|
|
|| currentmenuname == Menu::nodeathmodecomplete2)
|
|
|
|
{
|
|
|
|
music.play(Music_PRESENTINGVVVVVV);
|
|
|
|
}
|
Fix delayed notification of NDM unlock
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.
2024-01-10 10:33:10 +01:00
|
|
|
enum Menu::MenuName camefrom = currentmenuname;
|
Kludge-fix being able to play music in editor
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.
2021-09-11 03:56:12 +02:00
|
|
|
|
2020-04-17 04:16:40 +02:00
|
|
|
MenuStackFrame& frame = menustack[menustack.size()-1];
|
|
|
|
|
|
|
|
//Store this in case createmenu() removes the stack frame
|
|
|
|
int previousoption = frame.option;
|
|
|
|
|
|
|
|
createmenu(frame.name, true);
|
|
|
|
currentmenuoption = previousoption;
|
|
|
|
|
|
|
|
//Remove the stackframe now, but createmenu() might have already gotten to it
|
|
|
|
//if we were returning to the main menu
|
|
|
|
if (!menustack.empty())
|
|
|
|
{
|
|
|
|
menustack.pop_back();
|
|
|
|
}
|
Fix delayed notification of NDM unlock
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.
2024-01-10 10:33:10 +01:00
|
|
|
|
|
|
|
/* FIXME: Even more horrible kludge! */
|
|
|
|
if (camefrom == Menu::timetrialcomplete3)
|
|
|
|
{
|
|
|
|
if (can_unlock_ndm())
|
|
|
|
{
|
|
|
|
unlock_ndm();
|
|
|
|
}
|
|
|
|
}
|
2020-04-17 04:16:40 +02:00
|
|
|
}
|
|
|
|
|
2020-04-26 02:36:36 +02:00
|
|
|
void Game::returntomenu(enum Menu::MenuName t)
|
|
|
|
{
|
|
|
|
if (currentmenuname == t)
|
|
|
|
{
|
2020-05-19 00:19:56 +02:00
|
|
|
createmenu(t, true);
|
2020-04-26 02:36:36 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Unwind the menu stack until we reach our desired menu
|
|
|
|
int i = menustack.size() - 1;
|
|
|
|
while (i >= 0)
|
|
|
|
{
|
|
|
|
//If we pop it off we can't reference it anymore, so check for it now
|
|
|
|
bool is_the_menu_we_want = menustack[i].name == t;
|
|
|
|
|
|
|
|
returnmenu();
|
|
|
|
|
|
|
|
if (is_the_menu_we_want)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-17 04:04:36 +02:00
|
|
|
void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ )
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2022-12-30 22:57:24 +01:00
|
|
|
if (t == Menu::mainmenu && !menutestmode)
|
2020-04-17 04:04:36 +02:00
|
|
|
{
|
|
|
|
//Either we've just booted up the game or returned from gamemode
|
|
|
|
//Whichever it is, we shouldn't have a stack,
|
|
|
|
//and most likely don't have a current stackframe
|
|
|
|
menustack.clear();
|
|
|
|
}
|
|
|
|
else if (!samemenu)
|
|
|
|
{
|
|
|
|
MenuStackFrame frame;
|
|
|
|
frame.option = currentmenuoption;
|
|
|
|
frame.name = currentmenuname;
|
|
|
|
menustack.push_back(frame);
|
2021-03-05 09:42:55 +01:00
|
|
|
currentmenuoption = 0;
|
2020-04-17 04:04:36 +02:00
|
|
|
}
|
|
|
|
|
2020-01-01 21:29:24 +01:00
|
|
|
currentmenuname = t;
|
|
|
|
menuyoff = 0;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
int maxspacing = 30; // maximum value for menuspacing, can only become lower.
|
2020-01-01 21:29:24 +01:00
|
|
|
menucountdown = 0;
|
Refactor menu creation code
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().
2020-04-15 06:50:17 +02:00
|
|
|
menuoptions.clear();
|
2020-01-01 21:29:24 +01:00
|
|
|
|
2020-04-17 00:19:17 +02:00
|
|
|
switch (t)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::mainmenu:
|
2021-12-18 08:36:13 +01:00
|
|
|
if (ingame_titlemode)
|
|
|
|
{
|
|
|
|
/* We shouldn't be here! */
|
|
|
|
SDL_assert(0 && "Entering main menu from in-game options!");
|
|
|
|
break;
|
|
|
|
}
|
Refactor menu creation code
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().
2020-04-15 06:50:17 +02:00
|
|
|
#if !defined(MAKEANDPLAY)
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("play"));
|
Refactor menu creation code
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().
2020-04-15 06:50:17 +02:00
|
|
|
#endif
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("levels"));
|
|
|
|
option(loc::gettext("options"));
|
2022-12-30 22:57:24 +01:00
|
|
|
if (loc::show_translator_menu)
|
|
|
|
{
|
|
|
|
option(loc::gettext("translator"));
|
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("credits"));
|
|
|
|
option(loc::gettext("quit"));
|
2020-04-02 22:01:55 +02:00
|
|
|
menuyoff = -10;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::playerworlds:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("play a level"));
|
2023-08-23 19:51:11 +02:00
|
|
|
option(loc::gettext("level editor"), !editor_disabled);
|
|
|
|
if (!editor_disabled)
|
|
|
|
{
|
|
|
|
option(loc::gettext("open level folder"), FILESYSTEM_openDirectoryEnabled());
|
|
|
|
option(loc::gettext("show level folder path"));
|
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-02-10 03:21:19 +01:00
|
|
|
menuyoff = -40;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2021-12-22 09:58:27 +01:00
|
|
|
case Menu::confirmshowlevelspath:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("no, don't show me"));
|
|
|
|
option(loc::gettext("yes, reveal the path"));
|
2021-12-22 09:58:27 +01:00
|
|
|
menuyoff = -10;
|
|
|
|
break;
|
|
|
|
case Menu::showlevelspath:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return to levels"));
|
2021-12-22 09:58:27 +01:00
|
|
|
menuyoff = 60;
|
|
|
|
break;
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::levellist:
|
2021-02-21 00:40:11 +01:00
|
|
|
if(cl.ListOfMetaData.size()==0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("ok"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = -20;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-02-21 00:40:11 +01:00
|
|
|
for(int i=0; i<(int) cl.ListOfMetaData.size(); i++) // FIXME: int/size_t! -flibit
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
if(i>=levelpage*8 && i< (levelpage*8)+8)
|
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
const std::string filename = cl.ListOfMetaData[i].filename.substr(7);
|
|
|
|
int score = 0;
|
|
|
|
if (customlevelstats.count(filename) > 0)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
score = customlevelstats[filename];
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-07-04 06:30:31 +02:00
|
|
|
const char* prefix;
|
2023-10-25 04:05:23 +02:00
|
|
|
switch (score)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-10-25 04:05:23 +02:00
|
|
|
case 0:
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-07-04 06:30:31 +02:00
|
|
|
static const char tmp[] = " ";
|
|
|
|
prefix = tmp;
|
2023-10-25 04:05:23 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 1:
|
|
|
|
{
|
|
|
|
static const char tmp[] = " * ";
|
|
|
|
prefix = tmp;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 3:
|
|
|
|
{
|
|
|
|
static const char tmp[] = "** ";
|
|
|
|
prefix = tmp;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
SDL_assert(0 && "Unhandled menu text prefix!");
|
|
|
|
prefix = "";
|
|
|
|
break;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2024-02-03 00:58:46 +01:00
|
|
|
const char* title = cl.ListOfMetaData[i].title.c_str();
|
|
|
|
if (cl.ListOfMetaData[i].title_is_gettext)
|
|
|
|
{
|
|
|
|
title = loc::gettext(title);
|
|
|
|
}
|
2024-01-05 02:04:27 +01:00
|
|
|
/* We have to make sure the stars and spaces are consistently on the
|
|
|
|
* correct side of the title, no matter what bidi characters are in there.
|
|
|
|
* So just always let the bidi engine handle it, with a few control chars. */
|
2021-03-07 03:14:44 +01:00
|
|
|
char text[MENU_TEXT_BYTES];
|
2024-01-05 02:04:27 +01:00
|
|
|
SDL_snprintf(
|
|
|
|
text, sizeof(text),
|
|
|
|
"%s%s%s%s%s",
|
|
|
|
// LRM or RLM depending on UI language, to make the stars aligned to left or right
|
|
|
|
UTF8_encode(font::is_rtl(PR_FONT_INTERFACE) ? 0x200F : 0x200E).bytes,
|
|
|
|
prefix,
|
|
|
|
// FIRST STRONG ISOLATE, to start an isolated block oriented however bidi sees fit
|
|
|
|
UTF8_encode(0x2068).bytes,
|
2024-02-03 00:58:46 +01:00
|
|
|
title,
|
2024-01-05 02:04:27 +01:00
|
|
|
// POP DIRECTIONAL ISOLATE, exit isolated level title
|
|
|
|
UTF8_encode(0x2069).bytes
|
|
|
|
);
|
2021-02-16 04:18:46 +01:00
|
|
|
for (size_t ii = 0; text[ii] != '\0'; ++ii)
|
2020-07-03 23:54:23 +02:00
|
|
|
{
|
|
|
|
text[ii] = SDL_tolower(text[ii]);
|
|
|
|
}
|
2023-01-20 03:56:17 +01:00
|
|
|
option(
|
|
|
|
text,
|
|
|
|
true,
|
2024-01-05 02:04:27 +01:00
|
|
|
(cl.ListOfMetaData[i].title_is_gettext ? PR_FONT_INTERFACE : PR_FONT_IDX(
|
|
|
|
cl.ListOfMetaData[i].level_main_font_idx, font::is_rtl(PR_FONT_INTERFACE)
|
|
|
|
)) | PR_RTL_XFLIP
|
2023-01-20 03:56:17 +01:00
|
|
|
);
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-21 00:40:11 +01:00
|
|
|
if (cl.ListOfMetaData.size() > 8)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2021-02-21 00:40:11 +01:00
|
|
|
if((size_t) ((levelpage*8)+8) <cl.ListOfMetaData.size())
|
2021-08-12 04:48:40 +02:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("next page"));
|
2021-08-12 04:48:40 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("first page"));
|
2021-08-12 04:48:40 +02:00
|
|
|
}
|
|
|
|
if (levelpage == 0)
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("last page"));
|
2021-08-12 04:48:40 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("previous page"));
|
2021-08-12 04:48:40 +02:00
|
|
|
}
|
2020-04-17 08:05:49 +02:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
menuxoff = 20;
|
Refactor menu creation code
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().
2020-04-15 06:50:17 +02:00
|
|
|
menuyoff = 70-(menuoptions.size()*10);
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
menuspacing = 5;
|
|
|
|
return; // skip automatic centering, will turn out bad with levels list
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::quickloadlevel:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("continue from save"));
|
|
|
|
option(loc::gettext("start from beginning"));
|
|
|
|
option(loc::gettext("delete save"));
|
|
|
|
option(loc::gettext("back to levels"));
|
2020-02-10 01:53:01 +01:00
|
|
|
menuyoff = -30;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2021-08-12 05:26:58 +02:00
|
|
|
case Menu::deletequicklevel:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("no! don't delete"));
|
|
|
|
option(loc::gettext("yes, delete save"));
|
2021-08-12 05:26:58 +02:00
|
|
|
menuyoff = 64;
|
|
|
|
break;
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::youwannaquit:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("yes, quit"));
|
|
|
|
option(loc::gettext("no, return"));
|
2020-02-10 01:53:01 +01:00
|
|
|
menuyoff = -20;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::errornostart:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("ok"));
|
2020-02-10 01:53:01 +01:00
|
|
|
menuyoff = -20;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2021-04-09 17:53:55 +02:00
|
|
|
case Menu::gameplayoptions:
|
2021-04-09 12:09:12 +02:00
|
|
|
#if !defined(MAKEANDPLAY)
|
2023-06-06 02:34:27 +02:00
|
|
|
if (ingame_titlemode && unlock[Unlock_FLIPMODE])
|
2021-04-09 12:09:12 +02:00
|
|
|
#endif
|
2021-04-09 17:53:55 +02:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("flip mode"));
|
2021-04-09 17:53:55 +02:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("toggle fps"));
|
|
|
|
option(loc::gettext("speedrun options"));
|
|
|
|
option(loc::gettext("advanced options"));
|
|
|
|
option(loc::gettext("clear main game data"));
|
|
|
|
option(loc::gettext("clear custom level data"));
|
|
|
|
option(loc::gettext("return"));
|
2021-04-09 17:53:55 +02:00
|
|
|
menuyoff = -10;
|
|
|
|
maxspacing = 15;
|
|
|
|
break;
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::graphicoptions:
|
2021-12-26 07:55:55 +01:00
|
|
|
if (!gameScreen.isForcedFullscreen())
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("toggle fullscreen"));
|
2021-12-26 07:55:55 +01:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("scaling mode"));
|
2021-12-26 07:55:55 +01:00
|
|
|
if (!gameScreen.isForcedFullscreen())
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("resize to nearest"), gameScreen.isWindowed);
|
2021-12-26 07:55:55 +01:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("toggle filter"));
|
|
|
|
option(loc::gettext("toggle analogue"));
|
|
|
|
option(loc::gettext("toggle vsync"));
|
|
|
|
option(loc::gettext("return"));
|
Add "resize to nearest" graphics option
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.
2020-06-30 07:02:21 +02:00
|
|
|
menuyoff = -10;
|
2021-04-09 17:53:55 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::ed_settings:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("change description"));
|
|
|
|
option(loc::gettext("edit scripts"));
|
|
|
|
option(loc::gettext("change music"));
|
|
|
|
option(loc::gettext("editor ghosts"));
|
|
|
|
option(loc::gettext("load level"));
|
|
|
|
option(loc::gettext("save level"));
|
|
|
|
option(loc::gettext("options"));
|
|
|
|
option(loc::gettext("quit to main menu"));
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
menuyoff = -20;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::ed_desc:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("change name"));
|
|
|
|
option(loc::gettext("change author"));
|
|
|
|
option(loc::gettext("change description"));
|
|
|
|
option(loc::gettext("change website"));
|
2023-01-21 19:06:30 +01:00
|
|
|
option(loc::gettext("change font"));
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
|
|
|
|
menuyoff = 6;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::ed_music:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("next song"));
|
|
|
|
option(loc::gettext("previous song"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 16;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::ed_quit:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("yes, save and quit"));
|
|
|
|
option(loc::gettext("no, quit without saving"));
|
|
|
|
option(loc::gettext("return to editor"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 8;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2023-01-21 19:06:30 +01:00
|
|
|
case Menu::ed_font:
|
|
|
|
{
|
|
|
|
int option_match = -1;
|
|
|
|
for (uint8_t i = 0; i < font::font_idx_options_n; i++)
|
|
|
|
{
|
|
|
|
uint8_t idx = font::font_idx_options[i];
|
2024-01-03 22:54:20 +01:00
|
|
|
option(font::get_main_font_display_name(idx), true, PR_FONT_IDX(idx, font::is_rtl(PR_FONT_INTERFACE)) | PR_RTL_XFLIP);
|
2023-01-21 19:06:30 +01:00
|
|
|
if (font::level_font_is_main_idx(idx))
|
|
|
|
{
|
|
|
|
option_match = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
currentmenuoption = option_match != -1 ? option_match : 0;
|
|
|
|
maxspacing = 15;
|
|
|
|
break;
|
|
|
|
}
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::options:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("gameplay"));
|
|
|
|
option(loc::gettext("graphics"));
|
|
|
|
option(loc::gettext("audio"));
|
|
|
|
option(loc::gettext("game pad"));
|
|
|
|
option(loc::gettext("accessibility"));
|
2024-01-22 08:43:03 +01:00
|
|
|
option(loc::gettext("language"), !translator_cutscene_test);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-06-30 22:06:19 +02:00
|
|
|
menuyoff = 0;
|
2021-04-09 17:53:55 +02:00
|
|
|
maxspacing = 15;
|
|
|
|
break;
|
|
|
|
case Menu::speedrunneroptions:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("glitchrunner mode"));
|
|
|
|
option(loc::gettext("input delay"));
|
|
|
|
option(loc::gettext("interact button"));
|
|
|
|
option(loc::gettext("fake load screen"));
|
|
|
|
option(loc::gettext("toggle in-game timer"));
|
2023-10-02 02:44:13 +02:00
|
|
|
option(loc::gettext("english sprites"));
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2021-04-09 17:53:55 +02:00
|
|
|
menuyoff = 0;
|
|
|
|
maxspacing = 15;
|
2020-06-30 22:06:19 +02:00
|
|
|
break;
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
case Menu::setglitchrunner:
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("none"));
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
|
|
|
|
for (i = 1; i < GlitchrunnerNumVersions; ++i)
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext(GlitchrunnerMode_enum_to_string((enum GlitchrunnerMode) i)));
|
Split glitchrunner mode into multiple versions
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.
2021-08-05 02:09:49 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2020-06-30 22:06:19 +02:00
|
|
|
case Menu::advancedoptions:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("unfocus pause"));
|
|
|
|
option(loc::gettext("unfocus audio pause"));
|
|
|
|
option(loc::gettext("room name background"));
|
|
|
|
option(loc::gettext("return"));
|
2021-04-12 00:18:35 +02:00
|
|
|
menuyoff = 0;
|
|
|
|
maxspacing = 15;
|
|
|
|
break;
|
|
|
|
case Menu::audiooptions:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("music volume"));
|
|
|
|
option(loc::gettext("sound volume"));
|
2021-04-12 00:18:35 +02:00
|
|
|
if (music.mmmmmm)
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("soundtrack"));
|
2021-04-12 00:18:35 +02:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-04-02 22:01:55 +02:00
|
|
|
menuyoff = 0;
|
2021-04-09 17:53:55 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::accessibility:
|
2021-04-09 12:09:12 +02:00
|
|
|
#if !defined(MAKEANDPLAY)
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("unlock play modes"));
|
2021-04-09 12:09:12 +02:00
|
|
|
#endif
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("invincibility"), !ingame_titlemode || !incompetitive());
|
|
|
|
option(loc::gettext("slowdown"), !ingame_titlemode || !incompetitive());
|
|
|
|
option(loc::gettext("animated backgrounds"));
|
|
|
|
option(loc::gettext("screen effects"));
|
|
|
|
option(loc::gettext("text outline"));
|
|
|
|
option(loc::gettext("return"));
|
2020-06-30 22:06:19 +02:00
|
|
|
menuyoff = 0;
|
2021-04-09 17:53:55 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::controller:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("analog stick sensitivity"));
|
|
|
|
option(loc::gettext("bind flip"));
|
|
|
|
option(loc::gettext("bind enter"));
|
|
|
|
option(loc::gettext("bind menu"));
|
|
|
|
option(loc::gettext("bind restart"));
|
Show correct button glyph for interact if Enter/E are not split
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.
2023-03-26 05:19:01 +02:00
|
|
|
option(loc::gettext("bind interact"), separate_interact);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2021-04-09 12:09:12 +02:00
|
|
|
menuyoff = 0;
|
2021-04-09 17:53:55 +02:00
|
|
|
maxspacing = 10;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2022-12-30 22:57:24 +01:00
|
|
|
case Menu::language:
|
|
|
|
if (loc::languagelist.empty())
|
|
|
|
{
|
|
|
|
option(loc::gettext("ok"));
|
|
|
|
menuyoff = -20;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < loc::languagelist.size(); i++)
|
|
|
|
{
|
|
|
|
if (loc::languagelist[i].nativename.empty())
|
2024-01-03 20:09:23 +01:00
|
|
|
{
|
2022-12-30 22:57:24 +01:00
|
|
|
option(loc::languagelist[i].code.c_str());
|
2024-01-03 20:09:23 +01:00
|
|
|
}
|
2022-12-30 22:57:24 +01:00
|
|
|
else
|
2024-01-03 20:09:23 +01:00
|
|
|
{
|
|
|
|
option(
|
|
|
|
loc::languagelist[i].nativename.c_str(),
|
|
|
|
true,
|
|
|
|
PR_FONT_IDX(loc::languagelist[i].font_idx, loc::languagelist[i].rtl)
|
|
|
|
);
|
|
|
|
}
|
2022-12-30 22:57:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
menuyoff = 70-(menuoptions.size()*10);
|
|
|
|
maxspacing = 5;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case Menu::translator_main:
|
|
|
|
option(loc::gettext("translator options"));
|
|
|
|
option(loc::gettext("maintenance"));
|
|
|
|
option(loc::gettext("open lang folder"), FILESYSTEM_openDirectoryEnabled());
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 0;
|
|
|
|
break;
|
|
|
|
case Menu::translator_options:
|
|
|
|
option(loc::gettext("language statistics"));
|
|
|
|
option(loc::gettext("translate room names"));
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
option(loc::gettext("explore game"));
|
2022-12-30 22:57:24 +01:00
|
|
|
option(loc::gettext("menu test"));
|
2022-12-24 04:16:56 +01:00
|
|
|
option(loc::gettext("cutscene test"), loc::lang != "en");
|
2022-12-30 22:57:24 +01:00
|
|
|
option(loc::gettext("limits check"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 0;
|
|
|
|
break;
|
|
|
|
case Menu::translator_options_limitscheck:
|
|
|
|
option(loc::gettext("next page"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 64;
|
|
|
|
break;
|
|
|
|
case Menu::translator_options_stats:
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 64;
|
|
|
|
break;
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
case Menu::translator_options_exploregame:
|
|
|
|
option(loc::gettext("space station 1"));
|
|
|
|
option(loc::gettext("the laboratory"));
|
|
|
|
option(loc::gettext("the tower"));
|
|
|
|
option(loc::gettext("space station 2"));
|
|
|
|
option(loc::gettext("the warp zone"));
|
|
|
|
option(loc::gettext("intermission 1"));
|
|
|
|
option(loc::gettext("intermission 2"));
|
|
|
|
option(loc::gettext("the final level"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = -20;
|
|
|
|
break;
|
2022-12-24 04:16:56 +01:00
|
|
|
case Menu::translator_options_cutscenetest:
|
|
|
|
for (
|
|
|
|
size_t i = (cutscenetest_menu_page*14);
|
|
|
|
i < (cutscenetest_menu_page*14)+14 && i < loc::testable_script_ids.size();
|
|
|
|
i++
|
|
|
|
)
|
|
|
|
{
|
|
|
|
option(loc::testable_script_ids[i].c_str());
|
|
|
|
}
|
|
|
|
if((cutscenetest_menu_page*14)+14 < loc::testable_script_ids.size())
|
|
|
|
{
|
|
|
|
option(loc::gettext("next page"));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
option(loc::gettext("first page"));
|
|
|
|
}
|
|
|
|
if (cutscenetest_menu_page == 0)
|
|
|
|
{
|
|
|
|
option(loc::gettext("last page"));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
option(loc::gettext("previous page"));
|
|
|
|
}
|
|
|
|
option(loc::gettext("from clipboard"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
|
|
|
|
menuxoff = 20;
|
|
|
|
menuyoff = 55-(menuoptions.size()*10);
|
|
|
|
menuspacing = 5;
|
|
|
|
return; // skip automatic centering, will turn out bad with scripts list
|
2022-12-30 22:57:24 +01:00
|
|
|
case Menu::translator_maintenance:
|
|
|
|
option(loc::gettext("sync language files"));
|
|
|
|
option(loc::gettext("global statistics"), false);
|
|
|
|
option(loc::gettext("global limits check"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 0;
|
|
|
|
break;
|
|
|
|
case Menu::translator_maintenance_sync:
|
|
|
|
option(loc::gettext("sync"));
|
|
|
|
option(loc::gettext("return"));
|
|
|
|
menuyoff = 64;
|
|
|
|
break;
|
|
|
|
case Menu::translator_error_setlangwritedir:
|
|
|
|
option(loc::gettext("ok"));
|
|
|
|
menuyoff = 10;
|
|
|
|
break;
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::cleardatamenu:
|
2021-08-12 05:54:02 +02:00
|
|
|
case Menu::clearcustomdatamenu:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("no! don't delete"));
|
|
|
|
option(loc::gettext("yes, delete everything"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::setinvincibility:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("no, return to options"));
|
|
|
|
option(loc::gettext("yes, enable"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2020-04-17 01:02:01 +02:00
|
|
|
case Menu::setslowdown:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("normal speed"));
|
|
|
|
option(loc::gettext("80% speed"));
|
|
|
|
option(loc::gettext("60% speed"));
|
|
|
|
option(loc::gettext("40% speed"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 16;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::unlockmenu:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("unlock time trials"));
|
2023-06-06 02:34:27 +02:00
|
|
|
option(loc::gettext("unlock intermissions"), !unlock[Unlock_INTERMISSION_REPLAYS]);
|
|
|
|
option(loc::gettext("unlock no death mode"), !unlock[Unlock_NODEATHMODE]);
|
|
|
|
option(loc::gettext("unlock flip mode"), !unlock[Unlock_FLIPMODE]);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("unlock ship jukebox"), (stat_trinkets<20));
|
2023-06-06 02:34:27 +02:00
|
|
|
option(loc::gettext("unlock secret lab"), !unlock[Unlock_SECRETLAB]);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = -20;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::credits:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("next page"));
|
|
|
|
option(loc::gettext("last page"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::credits2:
|
|
|
|
case Menu::credits25:
|
|
|
|
case Menu::credits3:
|
|
|
|
case Menu::credits4:
|
|
|
|
case Menu::credits5:
|
2023-12-06 00:34:05 +01:00
|
|
|
case Menu::credits_localisations_implementation:
|
|
|
|
case Menu::credits_localisations_translations:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("next page"));
|
|
|
|
option(loc::gettext("previous page"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::credits6:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("first page"));
|
|
|
|
option(loc::gettext("previous page"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::play:
|
2020-04-17 01:08:56 +02:00
|
|
|
{
|
2020-01-01 21:29:24 +01:00
|
|
|
//Ok, here's where the unlock stuff comes into it:
|
|
|
|
//First up, time trials:
|
2020-04-17 01:08:56 +02:00
|
|
|
int temp = 0;
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_SPACESTATION1_COMPLETE]
|
|
|
|
&& stat_trinkets >= 3
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_SPACESTATION1])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
if (unlock[Unlock_LABORATORY_COMPLETE]
|
|
|
|
&& stat_trinkets >= 6
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_LABORATORY])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
if (unlock[Unlock_TOWER_COMPLETE]
|
|
|
|
&& stat_trinkets >= 9
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_TOWER])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
if (unlock[Unlock_SPACESTATION2_COMPLETE]
|
|
|
|
&& stat_trinkets >= 12
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_SPACESTATION2])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
if (unlock[Unlock_WARPZONE_COMPLETE]
|
|
|
|
&& stat_trinkets >= 15
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_WARPZONE])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
if (unlock[UnlockTrophy_GAME_COMPLETE]
|
|
|
|
&& stat_trinkets >= 18
|
|
|
|
&& !unlocknotify[Unlock_TIMETRIAL_FINALLEVEL])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
if (temp > 0)
|
|
|
|
{
|
|
|
|
//you've unlocked a time trial!
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_SPACESTATION1_COMPLETE] && stat_trinkets >= 3)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_SPACESTATION1] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_SPACESTATION1] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_LABORATORY_COMPLETE] && stat_trinkets >= 6)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_LABORATORY] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_LABORATORY] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_TOWER_COMPLETE] && stat_trinkets >= 9)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_TOWER] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_TOWER] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_SPACESTATION2_COMPLETE] && stat_trinkets >= 12)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_SPACESTATION2] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_SPACESTATION2] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_WARPZONE_COMPLETE] && stat_trinkets >= 15)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_WARPZONE] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_WARPZONE] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[UnlockTrophy_GAME_COMPLETE] && stat_trinkets >= 18)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlocknotify[Unlock_TIMETRIAL_FINALLEVEL] = true;
|
|
|
|
unlock[Unlock_TIMETRIAL_FINALLEVEL] = true;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (temp == 1)
|
|
|
|
{
|
2020-08-03 06:37:52 +02:00
|
|
|
createmenu(Menu::unlocktimetrial, true);
|
2021-01-11 04:39:19 +01:00
|
|
|
savestatsandsettings();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
else if (temp > 1)
|
|
|
|
{
|
2020-08-03 06:37:52 +02:00
|
|
|
createmenu(Menu::unlocktimetrials, true);
|
2021-01-11 04:39:19 +01:00
|
|
|
savestatsandsettings();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//Alright, we haven't unlocked any time trials. How about no death mode?
|
Fix delayed notification of NDM unlock
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.
2024-01-10 10:33:10 +01:00
|
|
|
if (can_unlock_ndm())
|
|
|
|
{
|
|
|
|
unlock_ndm();
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
2020-04-15 21:11:33 +02:00
|
|
|
//Alright then! Flip mode?
|
2023-06-06 02:34:27 +02:00
|
|
|
else if (unlock[UnlockTrophy_GAME_COMPLETE]
|
|
|
|
&& !unlocknotify[Unlock_FLIPMODE])
|
2020-04-15 21:11:33 +02:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlock[Unlock_FLIPMODE] = true;
|
|
|
|
unlocknotify[Unlock_FLIPMODE] = true;
|
2020-08-03 06:37:52 +02:00
|
|
|
createmenu(Menu::unlockflipmode, true);
|
2021-01-11 04:39:19 +01:00
|
|
|
savestatsandsettings();
|
2020-04-15 21:11:33 +02:00
|
|
|
}
|
|
|
|
//What about the intermission levels?
|
2023-06-06 02:34:27 +02:00
|
|
|
else if (unlock[Unlock_INTERMISSION2_COMPLETE]
|
|
|
|
&& !unlocknotify[Unlock_INTERMISSION_REPLAYS])
|
2020-04-15 21:11:33 +02:00
|
|
|
{
|
2023-06-06 02:34:27 +02:00
|
|
|
unlock[Unlock_INTERMISSION_REPLAYS] = true;
|
|
|
|
unlocknotify[Unlock_INTERMISSION_REPLAYS] = true;
|
2020-08-03 06:37:52 +02:00
|
|
|
createmenu(Menu::unlockintermission, true);
|
2021-01-11 04:39:19 +01:00
|
|
|
savestatsandsettings();
|
2020-04-15 21:11:33 +02:00
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
else
|
|
|
|
{
|
2020-04-26 22:41:35 +02:00
|
|
|
if (save_exists())
|
2020-04-26 22:09:56 +02:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("continue"));
|
2020-04-26 22:09:56 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("new game"));
|
2020-04-26 22:09:56 +02:00
|
|
|
}
|
2020-04-16 04:38:42 +02:00
|
|
|
//ok, secret lab! no notification, but test:
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_SECRETLAB])
|
2020-04-16 04:38:42 +02:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("secret lab"));
|
2020-04-16 04:38:42 +02:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("play modes"));
|
2020-04-26 22:41:35 +02:00
|
|
|
if (save_exists())
|
2020-04-26 22:09:56 +02:00
|
|
|
{
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("new game"));
|
2020-04-26 22:09:56 +02:00
|
|
|
}
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2023-06-06 02:34:27 +02:00
|
|
|
if (unlock[Unlock_SECRETLAB])
|
2020-04-16 04:38:42 +02:00
|
|
|
{
|
|
|
|
menuyoff = -30;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
menuyoff = -40;
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
}
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2020-04-17 01:08:56 +02:00
|
|
|
}
|
2020-04-17 00:19:17 +02:00
|
|
|
case Menu::unlocktimetrial:
|
|
|
|
case Menu::unlocktimetrials:
|
|
|
|
case Menu::unlocknodeathmode:
|
|
|
|
case Menu::unlockintermission:
|
|
|
|
case Menu::unlockflipmode:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("proceed"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 70;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::newgamewarning:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("start new game"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 64;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::playmodes:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("time trials"), !nocompetitive_unless_translator());
|
2023-06-06 02:34:27 +02:00
|
|
|
option(loc::gettext("intermissions"), unlock[Unlock_INTERMISSION_REPLAYS]);
|
|
|
|
option(loc::gettext("no death mode"), unlock[Unlock_NODEATHMODE] && !nocompetitive());
|
|
|
|
option(loc::gettext("flip mode"), unlock[Unlock_FLIPMODE]);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 8;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 20;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::intermissionmenu:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("play intermission 1"));
|
|
|
|
option(loc::gettext("play intermission 2"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = -35;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::playint1:
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
start_translator_exploring = false;
|
2022-12-07 21:03:01 +01:00
|
|
|
option(loc::gettext_case("Vitellary", 1));
|
|
|
|
option(loc::gettext_case("Vermilion", 1));
|
|
|
|
option(loc::gettext_case("Verdigris", 1));
|
|
|
|
option(loc::gettext_case("Victoria", 1));
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 10;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::playint2:
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
start_translator_exploring = false;
|
2022-12-07 21:03:01 +01:00
|
|
|
option(loc::gettext_case("Vitellary", 1));
|
|
|
|
option(loc::gettext_case("Vermilion", 1));
|
|
|
|
option(loc::gettext_case("Verdigris", 1));
|
|
|
|
option(loc::gettext_case("Victoria", 1));
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 10;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::continuemenu:
|
2020-04-17 04:52:39 +02:00
|
|
|
map.settowercolour(3);
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("continue from teleporter"));
|
|
|
|
option(loc::gettext("continue from quicksave"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 20;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::startnodeathmode:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("disable cutscenes"));
|
|
|
|
option(loc::gettext("enable cutscenes"));
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 40;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::gameover:
|
2020-01-01 21:29:24 +01:00
|
|
|
menucountdown = 120;
|
2020-04-16 06:53:36 +02:00
|
|
|
menudest=Menu::gameover2;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::gameover2:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return to play menu"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 80;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::unlockmenutrials:
|
2023-06-06 02:34:27 +02:00
|
|
|
option(loc::gettext("space station 1"), !unlock[Unlock_TIMETRIAL_SPACESTATION1]);
|
|
|
|
option(loc::gettext("the laboratory"), !unlock[Unlock_TIMETRIAL_LABORATORY]);
|
|
|
|
option(loc::gettext("the tower"), !unlock[Unlock_TIMETRIAL_TOWER]);
|
|
|
|
option(loc::gettext("space station 2"), !unlock[Unlock_TIMETRIAL_SPACESTATION2]);
|
|
|
|
option(loc::gettext("the warp zone"), !unlock[Unlock_TIMETRIAL_WARPZONE]);
|
|
|
|
option(loc::gettext("the final level"), !unlock[Unlock_TIMETRIAL_FINALLEVEL]);
|
2022-12-30 23:32:38 +01:00
|
|
|
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 0;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::timetrials:
|
2023-06-06 02:34:27 +02:00
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_SPACESTATION1] ? "space station 1" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_SPACESTATION1]);
|
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_LABORATORY] ? "the laboratory" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_LABORATORY]);
|
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_TOWER] ? "the tower" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_TOWER]);
|
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_SPACESTATION2] ? "space station 2" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_SPACESTATION2]);
|
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_WARPZONE] ? "the warp zone" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_WARPZONE]);
|
|
|
|
option(loc::gettext(unlock[Unlock_TIMETRIAL_FINALLEVEL] ? "the final level" : "???"),
|
|
|
|
unlock[Unlock_TIMETRIAL_FINALLEVEL]);
|
2022-12-30 23:32:38 +01:00
|
|
|
|
|
|
|
option(loc::gettext("return"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 0;
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
maxspacing = 15;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::nodeathmodecomplete:
|
2020-01-01 21:29:24 +01:00
|
|
|
menucountdown = 90;
|
2020-04-16 06:53:36 +02:00
|
|
|
menudest = Menu::nodeathmodecomplete2;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::nodeathmodecomplete2:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return to play menu"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 70;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::timetrialcomplete:
|
2020-01-01 21:29:24 +01:00
|
|
|
menucountdown = 90;
|
2020-04-16 06:53:36 +02:00
|
|
|
menudest=Menu::timetrialcomplete2;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::timetrialcomplete2:
|
2020-01-01 21:29:24 +01:00
|
|
|
menucountdown = 60;
|
2020-04-16 06:53:36 +02:00
|
|
|
menudest=Menu::timetrialcomplete3;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::timetrialcomplete3:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return to play menu"));
|
|
|
|
option(loc::gettext("try again"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 70;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
|
|
|
case Menu::gamecompletecontinue:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("return to play menu"));
|
2020-01-01 21:29:24 +01:00
|
|
|
menuyoff = 70;
|
2020-04-17 00:19:17 +02:00
|
|
|
break;
|
2020-11-22 03:10:26 +01:00
|
|
|
case Menu::errorsavingsettings:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("ok"));
|
|
|
|
option(loc::gettext("silence"));
|
2020-11-22 03:10:26 +01:00
|
|
|
menuyoff = 10;
|
|
|
|
break;
|
2021-08-07 05:57:34 +02:00
|
|
|
case Menu::errorloadinglevel:
|
2021-08-07 07:26:48 +02:00
|
|
|
case Menu::warninglevellist:
|
2022-12-30 23:32:38 +01:00
|
|
|
option(loc::gettext("ok"));
|
2021-08-07 07:26:48 +02:00
|
|
|
menuyoff = 50;
|
2021-08-07 05:57:34 +02:00
|
|
|
break;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
|
|
|
|
// Automatically center the menu. We must check the width of the menu with the initial horizontal spacing.
|
2020-06-29 23:18:33 +02:00
|
|
|
// If it's too wide, reduce the horizontal spacing by 5 and retry.
|
|
|
|
// Try to limit the menu width to 272 pixels: 320 minus 16*2 for square brackets, minus 8*2 padding.
|
|
|
|
// The square brackets fall outside the menu width (i.e. selected menu options are printed 16 pixels to the left)
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
bool done_once = false;
|
|
|
|
int menuwidth = 0;
|
2020-06-29 02:58:38 +02:00
|
|
|
for (; !done_once || (menuwidth > 272 && menuspacing > 0); maxspacing -= 5)
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
{
|
|
|
|
done_once = true;
|
|
|
|
menuspacing = maxspacing;
|
|
|
|
menuwidth = 0;
|
|
|
|
for (size_t i = 0; i < menuoptions.size(); i++)
|
|
|
|
{
|
2023-01-16 21:29:50 +01:00
|
|
|
int width = i*menuspacing + font::len(menuoptions[i].print_flags, menuoptions[i].text);
|
Make menus automatically centered and narrowed
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.
2020-06-29 02:09:52 +02:00
|
|
|
if (width > menuwidth)
|
|
|
|
menuwidth = width;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
menuxoff = (320-menuwidth)/2;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Fix delayed notification of NDM unlock
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.
2024-01-10 10:33:10 +01:00
|
|
|
bool Game::can_unlock_ndm(void)
|
|
|
|
{
|
|
|
|
int temp = 0;
|
|
|
|
if (bestrank[TimeTrial_SPACESTATION1] >= 2) temp++;
|
|
|
|
if (bestrank[TimeTrial_LABORATORY] >= 2) temp++;
|
|
|
|
if (bestrank[TimeTrial_TOWER] >= 2) temp++;
|
|
|
|
if (bestrank[TimeTrial_SPACESTATION2] >= 2) temp++;
|
|
|
|
if (bestrank[TimeTrial_WARPZONE] >= 2) temp++;
|
|
|
|
if (bestrank[TimeTrial_FINALLEVEL] >= 2) temp++;
|
|
|
|
return temp >= 4 && !unlocknotify[Unlock_NODEATHMODE];
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::unlock_ndm(void)
|
|
|
|
{
|
|
|
|
unlocknotify[Unlock_NODEATHMODE] = true;
|
|
|
|
unlock[Unlock_NODEATHMODE] = true;
|
|
|
|
createmenu(Menu::unlocknodeathmode, true);
|
|
|
|
savestatsandsettings();
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::deletequick(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-05-22 21:14:01 +02:00
|
|
|
if (inspecial() || map.custommode)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (!FILESYSTEM_delete("saves/qsave.vvv"))
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting saves/qsave.vvv");
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
}
|
2020-04-26 22:15:38 +02:00
|
|
|
else
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
{
|
|
|
|
SDL_zero(last_quicksave);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::deletetele(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2023-05-22 21:14:01 +02:00
|
|
|
if (inspecial() || map.custommode)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
if (!FILESYSTEM_delete("saves/tsave.vvv"))
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting saves/tsave.vvv");
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
}
|
2020-04-26 22:15:38 +02:00
|
|
|
else
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
{
|
|
|
|
SDL_zero(last_telesave);
|
|
|
|
}
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
2021-08-12 05:26:58 +02:00
|
|
|
void Game::customdeletequick(const std::string& file)
|
|
|
|
{
|
|
|
|
const std::string path = "saves/" + file.substr(7) + ".vvv";
|
|
|
|
|
|
|
|
if (!FILESYSTEM_delete(path.c_str()))
|
|
|
|
{
|
2021-02-24 00:21:29 +01:00
|
|
|
vlog_error("Error deleting %s", path.c_str());
|
2021-08-12 05:26:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::swnpenalty(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
//set the SWN clock back to the closest 5 second interval
|
|
|
|
if (swntimer <= 150)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 150) swntimer = 150;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 300)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 300) swntimer = 300;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 450)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 450) swntimer = 450;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 600)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 600) swntimer = 600;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 750)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 750) swntimer = 750;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 900)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 900) swntimer = 900;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1050)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1050) swntimer = 1050;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1200)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1200) swntimer = 1200;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1350)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1350) swntimer = 1350;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1500)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1500) swntimer = 1500;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1650)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1650) swntimer = 1650;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 1800)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 1800) swntimer = 1800;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 2100)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 2100) swntimer = 2100;
|
|
|
|
}
|
|
|
|
else if (swntimer <= 2400)
|
|
|
|
{
|
|
|
|
swntimer += 8;
|
|
|
|
if (swntimer > 2400) swntimer = 2400;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
int Game::crewrescued(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
2020-04-10 01:01:53 +02:00
|
|
|
int temp = 0;
|
2020-07-03 03:10:52 +02:00
|
|
|
for (size_t i = 0; i < SDL_arraysize(crewstats); i++)
|
2020-04-10 01:01:53 +02:00
|
|
|
{
|
|
|
|
if (crewstats[i])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return temp;
|
2020-01-01 21:29:24 +01:00
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::resetgameclock(void)
|
2020-01-01 21:29:24 +01:00
|
|
|
{
|
|
|
|
frames = 0;
|
|
|
|
seconds = 0;
|
|
|
|
minutes = 0;
|
|
|
|
hours = 0;
|
|
|
|
}
|
2020-04-07 08:46:27 +02:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
int Game::trinkets(void)
|
2020-04-07 08:46:27 +02:00
|
|
|
{
|
2020-04-10 01:17:11 +02:00
|
|
|
int temp = 0;
|
2020-07-03 04:17:32 +02:00
|
|
|
for (size_t i = 0; i < SDL_arraysize(obj.collect); i++)
|
2020-04-10 01:17:11 +02:00
|
|
|
{
|
|
|
|
if (obj.collect[i])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return temp;
|
2020-04-07 08:46:27 +02:00
|
|
|
}
|
2020-04-07 08:53:32 +02:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
int Game::crewmates(void)
|
2020-04-07 08:53:32 +02:00
|
|
|
{
|
2020-04-10 01:17:52 +02:00
|
|
|
int temp = 0;
|
2020-07-03 04:17:32 +02:00
|
|
|
for (size_t i = 0; i < SDL_arraysize(obj.customcollect); i++)
|
2020-04-10 01:17:52 +02:00
|
|
|
{
|
|
|
|
if (obj.customcollect[i])
|
|
|
|
{
|
|
|
|
temp++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return temp;
|
2020-04-07 08:53:32 +02:00
|
|
|
}
|
2020-04-26 21:43:30 +02:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::anything_unlocked(void)
|
2020-04-26 21:43:30 +02:00
|
|
|
{
|
2020-07-03 01:45:22 +02:00
|
|
|
for (size_t i = 0; i < SDL_arraysize(unlock); i++)
|
2020-04-26 21:43:30 +02:00
|
|
|
{
|
|
|
|
if (unlock[i] &&
|
|
|
|
(i == 8 // Secret Lab
|
2020-06-28 11:12:56 +02:00
|
|
|
|| (i >= 9 && i <= 14) // any Time Trial
|
2020-04-26 21:43:30 +02:00
|
|
|
|| i == 16 // Intermission replays
|
|
|
|
|| i == 17 // No Death Mode
|
|
|
|
|| i == 18)) // Flip Mode
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2020-04-26 22:41:35 +02:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
bool Game::save_exists(void)
|
2020-04-26 22:41:35 +02:00
|
|
|
{
|
Replace std::string Game::telesummary and Game::quicksummary by Summary
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)
2023-09-12 14:36:58 +02:00
|
|
|
return last_telesave.exists || last_quicksave.exists;
|
2020-04-26 22:41:35 +02:00
|
|
|
}
|
2020-05-07 23:38:19 +02:00
|
|
|
|
Defer hardreset to end of frame in quittomenu
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.
2023-05-19 04:11:04 +02:00
|
|
|
static void hardreset(void)
|
|
|
|
{
|
|
|
|
script.hardreset();
|
|
|
|
}
|
|
|
|
|
2023-05-19 04:58:23 +02:00
|
|
|
static void returntoeditor_callback(void)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
game.returntoeditor();
|
|
|
|
ed.show_note(loc::gettext("Level quits to menu"));
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::quittomenu(void)
|
2020-05-07 23:38:19 +02:00
|
|
|
{
|
2023-05-21 00:22:08 +02:00
|
|
|
if (gamestate != EDITORMODE && map.custommode && !map.custommodeforreal)
|
2023-05-19 04:58:23 +02:00
|
|
|
{
|
|
|
|
/* We are playtesting! Go back to the editor
|
|
|
|
* instead of losing unsaved changes. */
|
|
|
|
/* This needs to be deferred, otherwise some state would persist. */
|
|
|
|
DEFER_CALLBACK(returntoeditor_callback);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-07 23:38:19 +02:00
|
|
|
gamestate = TITLEMODE;
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEIN;
|
2021-04-03 21:48:47 +02:00
|
|
|
FILESYSTEM_unmountAssets();
|
2022-12-30 22:57:24 +01:00
|
|
|
loc::unloadtext_custom();
|
Start rewrite of font system
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.
2023-01-02 05:14:53 +01:00
|
|
|
font::unload_custom();
|
2021-04-14 05:00:07 +02:00
|
|
|
cliplaytest = false;
|
2020-11-03 00:23:53 +01:00
|
|
|
graphics.titlebg.tdrawback = true;
|
2020-05-07 23:38:19 +02:00
|
|
|
graphics.flipmode = false;
|
|
|
|
//Don't be stuck on the summary screen,
|
|
|
|
//or "who do you want to play the level with?"
|
|
|
|
//or "do you want cutscenes?"
|
|
|
|
//or the confirm-load-quicksave menu
|
2022-12-24 04:16:56 +01:00
|
|
|
if (translator_cutscene_test)
|
|
|
|
{
|
|
|
|
returntomenu(Menu::translator_options_cutscenetest);
|
|
|
|
}
|
|
|
|
else if (translator_exploring)
|
Add level exploring menu for translators
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.
2022-11-26 03:33:17 +01:00
|
|
|
{
|
|
|
|
returntomenu(Menu::translator_options_exploregame);
|
|
|
|
}
|
|
|
|
else if (intimetrial)
|
2020-05-07 23:38:19 +02:00
|
|
|
{
|
|
|
|
returntomenu(Menu::timetrials);
|
|
|
|
}
|
2020-05-07 23:54:39 +02:00
|
|
|
else if (inintermission)
|
2020-05-07 23:38:19 +02:00
|
|
|
{
|
|
|
|
returntomenu(Menu::intermissionmenu);
|
|
|
|
}
|
2020-05-07 23:54:39 +02:00
|
|
|
else if (nodeathmode)
|
2020-05-07 23:38:19 +02:00
|
|
|
{
|
|
|
|
returntomenu(Menu::playmodes);
|
|
|
|
}
|
2020-05-07 23:54:39 +02:00
|
|
|
else if (map.custommode)
|
2020-05-07 23:38:19 +02:00
|
|
|
{
|
Clean up all exit paths to the menu to use common code
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
2021-01-07 23:20:37 +01:00
|
|
|
if (map.custommodeforreal)
|
|
|
|
{
|
|
|
|
returntomenu(Menu::levellist);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//Returning from editor
|
2023-08-25 03:15:04 +02:00
|
|
|
editor_disabled = !BUTTONGLYPHS_keyboard_is_available();
|
Clean up all exit paths to the menu to use common code
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
2021-01-07 23:20:37 +01:00
|
|
|
returntomenu(Menu::playerworlds);
|
|
|
|
}
|
2020-05-07 23:38:19 +02:00
|
|
|
}
|
|
|
|
else if (save_exists() || anything_unlocked())
|
|
|
|
{
|
|
|
|
returntomenu(Menu::play);
|
2020-05-19 02:49:35 +02:00
|
|
|
if (!insecretlab)
|
|
|
|
{
|
|
|
|
//Select "continue"
|
|
|
|
currentmenuoption = 0;
|
|
|
|
}
|
2020-05-07 23:38:19 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
createmenu(Menu::mainmenu);
|
|
|
|
}
|
Defer hardreset to end of frame in quittomenu
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.
2023-05-19 04:11:04 +02:00
|
|
|
/* We might not be at the end of the frame yet.
|
|
|
|
* If we hardreset() now, some state might still persist. */
|
|
|
|
DEFER_CALLBACK(hardreset);
|
2020-05-07 23:38:19 +02:00
|
|
|
}
|
2020-05-08 00:17:04 +02:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::returntolab(void)
|
2020-05-08 00:17:04 +02:00
|
|
|
{
|
|
|
|
gamestate = GAMEMODE;
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_START_FADEIN;
|
2020-05-08 00:36:38 +02:00
|
|
|
map.gotoroom(119, 107);
|
|
|
|
int player = obj.getplayer();
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(player, obj.entities))
|
2020-05-08 00:36:38 +02:00
|
|
|
{
|
|
|
|
obj.entities[player].xp = 132;
|
|
|
|
obj.entities[player].yp = 137;
|
|
|
|
}
|
|
|
|
gravitycontrol = 0;
|
|
|
|
|
|
|
|
savepoint = 0;
|
|
|
|
saverx = 119;
|
|
|
|
savery = 107;
|
|
|
|
savex = 132;
|
|
|
|
savey = 137;
|
|
|
|
savegc = 0;
|
Use explicit INBOUNDS_VEC() instead of checking sentinel -1
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".
2020-09-09 13:15:14 +02:00
|
|
|
if (INBOUNDS_VEC(player, obj.entities))
|
2020-05-08 00:36:38 +02:00
|
|
|
{
|
|
|
|
savedir = obj.entities[player].dir;
|
|
|
|
}
|
|
|
|
|
2023-05-24 03:37:32 +02:00
|
|
|
music.play(Music_PIPEDREAM);
|
2020-05-08 00:17:04 +02:00
|
|
|
}
|
2020-05-09 21:35:17 +02:00
|
|
|
|
Remove game.shouldreturntoeditor in favor of using defer callback
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.
2021-01-10 23:48:25 +01:00
|
|
|
static void resetbg(void)
|
|
|
|
{
|
|
|
|
graphics.backgrounddrawn = false;
|
|
|
|
}
|
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::returntoeditor(void)
|
2020-05-09 21:35:17 +02:00
|
|
|
{
|
2020-09-28 04:15:06 +02:00
|
|
|
gamestate = EDITORMODE;
|
2020-05-09 21:35:17 +02:00
|
|
|
|
2021-09-13 06:02:15 +02:00
|
|
|
graphics.textboxes.clear();
|
2020-09-28 04:15:06 +02:00
|
|
|
hascontrol = true;
|
|
|
|
advancetext = false;
|
|
|
|
completestop = false;
|
2022-12-07 00:20:48 +01:00
|
|
|
setstate(0);
|
2020-05-09 21:35:17 +02:00
|
|
|
graphics.showcutscenebars = false;
|
Enumify all fade modes
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.
2022-04-25 09:57:47 +02:00
|
|
|
graphics.fademode = FADE_NONE;
|
2020-05-09 21:35:17 +02:00
|
|
|
|
2020-07-11 09:04:54 +02:00
|
|
|
ed.keydelay = 6;
|
2020-07-11 09:09:02 +02:00
|
|
|
ed.settingskey = true;
|
2023-03-05 19:59:36 +01:00
|
|
|
ed.old_note_timer = 0;
|
|
|
|
ed.note_timer = 0;
|
2020-07-11 09:39:09 +02:00
|
|
|
ed.roomnamehide = 0;
|
2020-07-11 09:04:54 +02:00
|
|
|
|
2023-01-20 04:57:06 +01:00
|
|
|
// Might've been changed in a script
|
2023-01-21 03:27:55 +01:00
|
|
|
font::set_level_font(cl.level_font_name.c_str());
|
2023-01-20 04:57:06 +01:00
|
|
|
|
Remove game.shouldreturntoeditor in favor of using defer callback
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.
2021-01-10 23:48:25 +01:00
|
|
|
DEFER_CALLBACK(resetbg);
|
2020-05-09 21:35:17 +02:00
|
|
|
music.fadeout();
|
|
|
|
//If warpdir() is used during playtesting, we need to set it back after!
|
2021-02-21 00:40:11 +01:00
|
|
|
for (int j = 0; j < cl.maxheight; j++)
|
2020-05-09 21:35:17 +02:00
|
|
|
{
|
2021-02-21 00:40:11 +01:00
|
|
|
for (int i = 0; i < cl.maxwidth; i++)
|
2020-05-09 21:35:17 +02:00
|
|
|
{
|
2021-02-21 00:54:24 +01:00
|
|
|
cl.roomproperties[i+(j*cl.maxwidth)].warpdir=ed.kludgewarpdir[i+(j*cl.maxwidth)];
|
2020-05-09 21:35:17 +02:00
|
|
|
}
|
|
|
|
}
|
2020-11-03 00:23:53 +01:00
|
|
|
graphics.titlebg.scrolldir = 0;
|
2023-03-02 07:45:22 +01:00
|
|
|
graphics.backgrounddrawn = false;
|
|
|
|
graphics.foregrounddrawn = false;
|
2020-05-09 21:35:17 +02:00
|
|
|
}
|
2020-06-23 02:26:45 +02:00
|
|
|
|
2021-01-13 05:11:50 +01:00
|
|
|
static void returntoingametemp(void)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
game.returntomenu(game.kludge_ingametemp);
|
|
|
|
}
|
|
|
|
|
2021-03-19 05:11:52 +01:00
|
|
|
static void returntoedsettings(void)
|
|
|
|
{
|
|
|
|
extern Game game;
|
|
|
|
game.returntomenu(Menu::ed_settings);
|
|
|
|
}
|
|
|
|
|
2021-03-19 05:12:55 +01:00
|
|
|
static void nextbgcolor(void)
|
|
|
|
{
|
|
|
|
map.nexttowercolour();
|
|
|
|
}
|
|
|
|
|
2021-03-20 07:23:18 +01:00
|
|
|
static void setfademode(void)
|
|
|
|
{
|
|
|
|
graphics.fademode = graphics.ingame_fademode;
|
|
|
|
}
|
|
|
|
|
2021-05-12 05:03:11 +02:00
|
|
|
static void setflipmode(void)
|
|
|
|
{
|
|
|
|
graphics.flipmode = graphics.setflipmode;
|
|
|
|
}
|
|
|
|
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
void Game::returntoingame(void)
|
2020-06-23 02:26:45 +02:00
|
|
|
{
|
|
|
|
ingame_titlemode = false;
|
2020-09-28 04:15:06 +02:00
|
|
|
mapheld = true;
|
2023-08-23 19:51:11 +02:00
|
|
|
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
if (ingame_editormode)
|
Fix being able to circumvent not-in-Flip-Mode detection
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.
2020-07-11 01:30:28 +02:00
|
|
|
{
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
ingame_editormode = false;
|
2021-03-19 05:11:52 +01:00
|
|
|
DEFER_CALLBACK(returntoedsettings);
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
gamestate = EDITORMODE;
|
|
|
|
ed.settingskey = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-01-13 05:11:50 +01:00
|
|
|
DEFER_CALLBACK(returntoingametemp);
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
gamestate = MAPMODE;
|
2021-05-12 05:03:11 +02:00
|
|
|
DEFER_CALLBACK(setflipmode);
|
2021-03-20 07:23:18 +01:00
|
|
|
DEFER_CALLBACK(setfademode);
|
2021-09-01 00:33:20 +02:00
|
|
|
if (!map.custommode && !graphics.setflipmode)
|
Add graphic options and game options to editor settings
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.
2021-03-19 03:52:30 +01:00
|
|
|
{
|
|
|
|
obj.flags[73] = true;
|
|
|
|
}
|
Fix being able to circumvent not-in-Flip-Mode detection
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.
2020-07-11 01:30:28 +02:00
|
|
|
}
|
2021-03-19 05:12:55 +01:00
|
|
|
DEFER_CALLBACK(nextbgcolor);
|
2024-08-30 19:19:27 +02:00
|
|
|
|
|
|
|
if (nocompetitive())
|
|
|
|
{
|
|
|
|
invalidate_ndm_trophy();
|
|
|
|
}
|
2020-06-23 02:26:45 +02:00
|
|
|
}
|
2020-08-01 21:49:07 +02:00
|
|
|
|
2022-12-29 23:25:27 +01:00
|
|
|
void Game::unlockAchievement(const char* name)
|
|
|
|
{
|
2023-02-04 09:14:04 +01:00
|
|
|
#ifdef MAKEANDPLAY
|
|
|
|
UNUSED(name);
|
|
|
|
#else
|
2022-12-29 23:25:27 +01:00
|
|
|
if (map.custommode)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
vlog_debug("Achievement \"%s\" unlocked.", name);
|
|
|
|
NETWORK_unlockAchievement(name);
|
2020-08-01 21:49:07 +02:00
|
|
|
#endif
|
2020-08-01 22:04:37 +02:00
|
|
|
}
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
|
2021-12-18 08:57:55 +01:00
|
|
|
void Game::mapmenuchange(const enum GameGamestate newgamestate, const bool user_initiated)
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
{
|
2021-09-06 01:59:05 +02:00
|
|
|
if (user_initiated && graphics.resumegamemode)
|
2021-09-02 21:21:46 +02:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Fix bringing up map menu during gamemode(teleporter)
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.
2020-12-29 00:36:32 +01:00
|
|
|
prevgamestate = gamestate;
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
gamestate = newgamestate;
|
|
|
|
graphics.resumegamemode = false;
|
Fix bringing up map menu during gamemode(teleporter)
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.
2020-12-29 00:36:32 +01:00
|
|
|
mapheld = true;
|
2023-08-23 17:42:28 +02:00
|
|
|
gameScreen.recacheTextures();
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
|
Fix bringing up map menu during gamemode(teleporter)
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.
2020-12-29 00:36:32 +01:00
|
|
|
if (prevgamestate == GAMEMODE)
|
|
|
|
{
|
|
|
|
graphics.menuoffset = 240;
|
|
|
|
}
|
|
|
|
else
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
{
|
Fix bringing up map menu during gamemode(teleporter)
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.
2020-12-29 00:36:32 +01:00
|
|
|
graphics.menuoffset = 0;
|
De-duplicate menu animation code when bringing up map screen
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.
2020-12-28 23:23:35 +01:00
|
|
|
}
|
|
|
|
graphics.oldmenuoffset = graphics.menuoffset;
|
|
|
|
}
|
2021-01-08 01:18:07 +01:00
|
|
|
|
Explicitly declare void for all void parameter functions (#628)
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.
2021-02-25 23:23:59 +01:00
|
|
|
void Game::copyndmresults(void)
|
2021-01-08 01:18:07 +01:00
|
|
|
{
|
|
|
|
ndmresultcrewrescued = crewrescued();
|
|
|
|
ndmresulttrinkets = trinkets();
|
2024-01-22 03:32:18 +01:00
|
|
|
ndmresulthardestroom = hardestroom;
|
|
|
|
ndmresulthardestroom_x = hardestroom_x;
|
|
|
|
ndmresulthardestroom_y = hardestroom_y;
|
|
|
|
ndmresulthardestroom_specialname = hardestroom_specialname;
|
2021-01-08 01:18:07 +01:00
|
|
|
SDL_memcpy(ndmresultcrewstats, crewstats, sizeof(ndmresultcrewstats));
|
|
|
|
}
|
2021-04-02 20:50:30 +02:00
|
|
|
|
2024-08-30 19:19:27 +02:00
|
|
|
void Game::invalidate_ndm_trophy(void)
|
|
|
|
{
|
|
|
|
if (nodeatheligible)
|
|
|
|
{
|
|
|
|
vlog_debug("NDM trophy is invalidated!");
|
|
|
|
}
|
|
|
|
nodeatheligible = false;
|
|
|
|
}
|
|
|
|
|
2021-04-02 20:50:30 +02:00
|
|
|
static inline int get_framerate(const int slowdown)
|
|
|
|
{
|
|
|
|
switch (slowdown)
|
|
|
|
{
|
|
|
|
case 30:
|
|
|
|
return 34;
|
|
|
|
case 24:
|
|
|
|
return 41;
|
|
|
|
case 18:
|
|
|
|
return 55;
|
|
|
|
case 12:
|
|
|
|
return 83;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 34;
|
|
|
|
}
|
|
|
|
|
|
|
|
int Game::get_timestep(void)
|
|
|
|
{
|
2023-12-29 22:51:48 +01:00
|
|
|
if ((gamestate == GAMEMODE || (gamestate == TELEPORTERMODE && !useteleporter)) &&
|
|
|
|
level_debugger::is_active() &&
|
|
|
|
!level_debugger::is_pausing() &&
|
|
|
|
key.isDown(SDLK_f))
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2021-04-02 20:50:30 +02:00
|
|
|
switch (gamestate)
|
|
|
|
{
|
|
|
|
case GAMEMODE:
|
|
|
|
return get_framerate(slowdown);
|
|
|
|
default:
|
|
|
|
return 34;
|
|
|
|
}
|
|
|
|
}
|
2021-05-04 03:22:59 +02:00
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
bool Game::physics_frozen(void)
|
|
|
|
{
|
2023-07-02 04:02:25 +02:00
|
|
|
return roomname_translator::is_pausing() || level_debugger::is_pausing();
|
2022-12-30 22:57:24 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 03:22:59 +02:00
|
|
|
bool Game::incompetitive(void)
|
|
|
|
{
|
Move Secret Lab nocompetitive check to Super Gravitron
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.
2021-05-04 03:57:13 +02:00
|
|
|
return (
|
|
|
|
!map.custommode
|
|
|
|
&& swnmode
|
2023-06-05 08:24:31 +02:00
|
|
|
&& (swngame == SWN_SUPERGRAVITRON ||
|
|
|
|
swngame == SWN_START_SUPERGRAVITRON_STEP_1 ||
|
|
|
|
swngame == SWN_START_SUPERGRAVITRON_STEP_2)
|
Move Secret Lab nocompetitive check to Super Gravitron
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.
2021-05-04 03:57:13 +02:00
|
|
|
)
|
|
|
|
|| intimetrial
|
|
|
|
|| nodeathmode;
|
2021-05-04 03:22:59 +02:00
|
|
|
}
|
2021-05-04 03:29:23 +02:00
|
|
|
|
|
|
|
bool Game::nocompetitive(void)
|
|
|
|
{
|
|
|
|
return slowdown < 30 || map.invincibility;
|
|
|
|
}
|
2021-08-05 23:31:20 +02:00
|
|
|
|
2022-12-30 22:57:24 +01:00
|
|
|
bool Game::nocompetitive_unless_translator(void)
|
|
|
|
{
|
|
|
|
return slowdown < 30 || (map.invincibility && !roomname_translator::enabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Game::sabotage_time_trial(void)
|
|
|
|
{
|
|
|
|
timetrialcheater = true;
|
|
|
|
hours++;
|
|
|
|
deathcounts += 100;
|
|
|
|
timetrialparlost = true;
|
|
|
|
}
|
|
|
|
|
2023-03-18 23:24:14 +01:00
|
|
|
bool Game::isingamecompletescreen(void)
|
2021-08-05 23:31:20 +02:00
|
|
|
{
|
|
|
|
return (state >= 3501 && state <= 3518) || (state >= 3520 && state <= 3522);
|
|
|
|
}
|