[Feature] Implement options menu and reading/writing values from/to the SRAM (#449)

* background

* Implement Options button

* Update main menu artwork

* add options menu tiles

* prepare options menu handling

* incorporate main menu refactor

* toto

* add all menu interactions

* do not reset selection_x in main_menu

* options struct

* fix rebase from ricfehr3's repo

* Implement high contrast cards

* Save/Load options values to SRAM

* load options on game_init and validate data within the load_options function

* Get changes from ricfehr3's PR

* recover fix for glitched ace

* clang format

* clang format

* clang format

* Save 500B of image data by using only minimal set of tiles instead of keeping final window in source file

* clang format

* clang format

* Implement Options button

* Update main menu artwork

* progress, leaving it alone for now until discussion

* make game_vars an extern

* Move options vars into their own struct

* rollback previous commit

* Implement save validation. Version check to come

* implement data validity + version check for game saves

* clang format

* Save position in rng sequence for consistent run after load

* fix rebase issues

* fix rebase issue

* remove unused/duplicate define

* Oops, made a mistake when testing PR #424

* encode high contrast palette into an image, and load it at game_start

* document save.c/h and correct typos

* correct alignment

* game speed and high contrast buttons working

* small cleanup

* sliders also work again now

* clang format

* Moved balatro_version extern declaration to version.h file

* removed unsused variable

* make showing ROM version a toggle

* move ante, round and money to the GameVariables struct

* Rename SaveCheckInfo struct to SaveHeader for clarity

* Add SaveHeader struct explanation

* clang format

* Revert unnecessary tileset optimization

* SFX now follows the sound volume slider

* clang format

* Music now follows volume slider

* Changed volume slider increment from 5 to 20 to avoid having the music sound too rough under 20% volume

* refactor main_menu.c to use SelectionGrid and Button structs

* clang format

* make a "new" save button so we don't save during the state transition

* clang format

* Reduced state transition flicker by only changing affine background to a different one

* Fix loading game too late in game init sequence

* First batch of suggestions from @MeirGavish

* Remove unnecessary intermediary buffers for high contrast palette swapping

* comment row warp hack for button input

* Add some of Copilot's suggestions

* rename balatro_version into gbalatro_version for rebranding reasons + version display erase screen fix

* remove unnecessary NULL check on `on_selection_changed` method

* implement small game speed arrow buttons animation

* factorize some code

* remove unnecessary data validation in load_game(), since we already checked the header

* Implement @MeirGavish 's suggestion regarding splitting the huge `on_update` function

* clang format

* implement @ricfehr3 's suggestion about keeping last selected button instead of using an obscure `on_boot` boolean

* Apply @ricfehr3's suggestions

* fix clang-format flags

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
This commit is contained in:
Geralt
2026-05-02 23:28:52 +02:00
committed by GitHub
parent da988ba7f5
commit e7f550ce22
38 changed files with 1231 additions and 185 deletions
+3 -2
View File
@@ -572,7 +572,7 @@ EXTRACT_PACKAGE = NO
# included in the documentation.
# The default value is: NO.
EXTRACT_STATIC = NO
EXTRACT_STATIC = YES
# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO,
@@ -588,7 +588,7 @@ EXTRACT_LOCAL_CLASSES = YES
# included.
# The default value is: NO.
EXTRACT_LOCAL_METHODS = NO
EXTRACT_LOCAL_METHODS = YES
# If this flag is set to YES, the members of anonymous namespaces will be
# extracted and appear in the documentation as a namespace called
@@ -3023,3 +3023,4 @@ MSCFILE_DIRS =
EXCLUDE = build tests graphics audio doc scripts
USE_MDFILE_AS_MAINPAGE = CONTRIBUTING.md
+1 -1
View File
@@ -55,7 +55,7 @@ CFLAGS += $(INCLUDE)
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := -g $(ARCH)
LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map),--undefined=balatro_version
LDFLAGS = -g $(ARCH) -Wl,-Map,$(notdir $*.map),--undefined=gbalatro_version
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1 @@
-gB8 -mR8 -mLs -mRtf -pn 160
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+1 -1
View File
@@ -1 +1 @@
-gB4 -Mw4 -Mh4 -m!
-gB4 -Mw4 -Mh4 -m! -pn 16
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+1
View File
@@ -0,0 +1 @@
-gB4 -g! -m! -pn 16
Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

+7
View File
@@ -33,6 +33,13 @@
*/
enum AffineBackgroundID
{
/**
* @brief Display background for the initial splash screen.
*
* Signifies that no background is currently in use.
*/
AFFINE_BG_NONE,
/**
* @brief Display background for main menu.
*
+9 -3
View File
@@ -9,10 +9,16 @@
#include <mm_types.h>
/**
* @def MM_FULL_VOLUME
* @def MM_MODULE_FULL_VOLUME
* @brief The maximum volume for maxmod module volume (main theme)
*/
#define MM_MODULE_FULL_VOLUME 1024
/**
* @def MM_SFX_FULL_VOLUME
* @brief The maximum volume for maxmod mm_sound_effect.volume
*/
#define MM_FULL_VOLUME 255
#define MM_SFX_FULL_VOLUME 255
/**
* @def MM_PAN_CENTER
@@ -32,7 +38,7 @@
* @def SFX_DEFAULT_VOLUME
* @brief Default volume for sound effects
*/
#define SFX_DEFAULT_VOLUME MM_FULL_VOLUME
#define SFX_DEFAULT_VOLUME MM_SFX_FULL_VOLUME
/**
* @def SFX_DEFAULT_PAN
+1 -1
View File
@@ -11,7 +11,7 @@
#define BTN_HIGHLIGHT_COLOR 0xFFFF
#define BUTTON_SFX_VOLUME 154 // 60% of MM_FULL_VOLUME
#define BUTTON_SFX_VOLUME 154 // 60% of MM_SFX_FULL_VOLUME
/**
* @brief The function to be called when the button is pressed
+1
View File
@@ -56,6 +56,7 @@ typedef struct CardObject
// Card functions
void card_init();
void toggle_high_contrast_cards(bool enable);
// Card methods
Card* card_new(u8 suit, u8 rank);
+10 -9
View File
@@ -1,12 +1,13 @@
// clang-format off
// (stateEnum, on_init, on_update, on_exit)
DEF_STATE_INFO(GAME_STATE_SPLASH_SCREEN, splash_screen_on_init, splash_screen_on_update, splash_screen_on_exit)
DEF_STATE_INFO(GAME_STATE_MAIN_MENU, game_main_menu_on_init, game_main_menu_on_update, game_main_menu_on_exit)
DEF_STATE_INFO(GAME_STATE_GAME_START, noop, game_start, noop)
DEF_STATE_INFO(GAME_STATE_PLAYING, game_round_on_init, game_playing_on_update, noop)
DEF_STATE_INFO(GAME_STATE_ROUND_END, noop, game_round_end_on_update, game_round_end_on_exit)
DEF_STATE_INFO(GAME_STATE_SHOP, noop, game_shop_on_update, game_shop_on_exit)
DEF_STATE_INFO(GAME_STATE_BLIND_SELECT, game_blind_select_on_init, game_blind_select_on_update, game_blind_select_on_exit)
DEF_STATE_INFO(GAME_STATE_LOSE, game_lose_on_init, game_lose_on_update, game_over_on_exit)
DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_win_on_update, game_over_on_exit)
DEF_STATE_INFO(GAME_STATE_SPLASH_SCREEN, splash_screen_on_init, splash_screen_on_update, splash_screen_on_exit )
DEF_STATE_INFO(GAME_STATE_MAIN_MENU, game_main_menu_on_init, game_main_menu_on_update, game_main_menu_on_exit )
DEF_STATE_INFO(GAME_STATE_OPTIONS_MENU, game_options_menu_on_init, game_options_menu_on_update, game_options_menu_on_exit)
DEF_STATE_INFO(GAME_STATE_GAME_START, noop, game_start, noop )
DEF_STATE_INFO(GAME_STATE_PLAYING, game_round_on_init, game_playing_on_update, noop )
DEF_STATE_INFO(GAME_STATE_ROUND_END, noop, game_round_end_on_update, game_round_end_on_exit )
DEF_STATE_INFO(GAME_STATE_SHOP, noop, game_shop_on_update, game_shop_on_exit )
DEF_STATE_INFO(GAME_STATE_BLIND_SELECT, game_blind_select_on_init, game_blind_select_on_update, game_blind_select_on_exit)
DEF_STATE_INFO(GAME_STATE_LOSE, game_lose_on_init, game_lose_on_update, game_over_on_exit )
DEF_STATE_INFO(GAME_STATE_WIN, game_win_on_init, game_win_on_update, game_over_on_exit )
// clang-format on
+9 -9
View File
@@ -1,6 +1,7 @@
#ifndef GAME_H
#define GAME_H
#include "blind.h"
#include "game/common_ui.h"
#include "game_variables.h"
@@ -13,7 +14,7 @@ extern GameVariables g_game_vars;
#define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now.
#define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers
#define MAX_SELECTION_SIZE 5
#define FRAMES(x) (((x) + game_speed - 1) / game_speed)
#define FRAMES(x) (((x) + (g_game_vars.game_speed) - 1) / (g_game_vars.game_speed))
// TODO: Can make these dynamic to support interest-related jokers and vouchers
#define MAX_INTEREST 5
@@ -123,7 +124,7 @@ typedef struct ContainedHandTypes
} ContainedHandTypes;
// clang-format on
typedef void (*GameStateCallback)();
typedef void (*GameStateCallback)(void);
typedef struct
{
@@ -158,17 +159,16 @@ int get_num_hands_remaining(void);
u32 get_chips(void);
void set_chips(u32 new_chips);
void display_chips();
void display_chips(void);
u32 get_mult(void);
void set_mult(u32 new_mult);
void display_mult();
int get_money(void);
void set_money(int new_money);
void display_money();
void display_mult(void);
void display_money(void);
void set_retrigger(bool new_retrigger);
enum BlindType get_current_blind(void);
enum BlindType get_next_boss_blind(void);
int get_game_speed(void);
void set_game_speed(int new_game_speed);
u32 get_rand(void);
// joker specific functions
bool is_shortcut_joker_active(void);
+1
View File
@@ -17,6 +17,7 @@ enum BackgroundId
BG_ROUND_END,
BG_SHOP,
BG_BLIND_SELECT,
BG_OPTIONS_MENU,
BG_MAIN_MENU
};
+1 -7
View File
@@ -15,23 +15,17 @@ void game_main_menu_change_background(void);
/**
* @brief Main menu state initialization
*
* @param vars passed @ref GameVariables struct
*/
void game_main_menu_on_init(void);
/**
* @brief Main menu state update
*
* @param vars passed @ref GameVariables struct
*/
void game_main_menu_on_update(void);
/**
* @brief Main menu cleanup (called when transitioning to game start)
*
* @param vars passed @ref GameVariables struct
*/
void game_main_menu_on_exit(void);
#endif // GAME_MAIN_MENU_H
#endif // GAME_MAIN_MENU_H
+31
View File
@@ -0,0 +1,31 @@
/**
* @file options_menu.h
*
* @brief Options menu state functions.
*/
#ifndef GAME_OPTIONS_MENU_H
#define GAME_OPTIONS_MENU_H
#include "game_variables.h"
/**
* @brief Change the options menu background
*/
void game_options_menu_change_background(void);
/**
* @brief Options menu state initialization
*/
void game_options_menu_on_init(void);
/**
* @brief Options menu state update
*/
void game_options_menu_on_update(void);
/**
* @brief Options menu cleanup (called when going back to main menu)
*/
void game_options_menu_on_exit(void);
#endif // GAME_OPTIONS_MENU_H
+32 -1
View File
@@ -8,6 +8,19 @@
#include <tonc.h>
#define GAME_SPEED_MIN 1
#define GAME_SPEED_MAX 4
// Volume is stored from 0 to 5 but is an increment of 20 so 0 to 100 will be displayed
#define VOLUME_OPTION_MIN 0
#define VOLUME_OPTION_MAX 5
#define VOLUME_OPTION_INCREMENT 20
#define DEFAULT_GAME_SPEED 1
#define DEFAULT_HIGH_CONTRAST false
#define DEFAULT_MUSIC_VOLUME VOLUME_OPTION_MAX
#define DEFAULT_SOUND_VOLUME VOLUME_OPTION_MAX
/**
* @brief A central location for all game variables.
*
@@ -16,8 +29,26 @@
*/
typedef struct
{
s32 timer;
// Internal variables
s32 timer; // This might already exist in libtonc but idk so i'm just making my own
u32 rng_seed;
u32 rng_step; // Position in the rng sequence.
// Variables visible by the player
int round;
int ante;
int money;
// Options variables
// BY DEFAULT IS SET TO 1, but if changed to 2 or more, should speed up all (or most) of the
// game aspects that should be sped up by speed, as in the original game.
u8 game_speed;
bool high_contrast;
u8 music_volume;
u8 sound_volume;
} GameVariables;
extern GameVariables g_game_vars;
+25
View File
@@ -0,0 +1,25 @@
/**
* @file save.h
*
* @brief Utils functions to save/load data structures from/to the SRAM.
*/
#ifndef SAVE_H
#define SAVE_H
#include "game_variables.h"
#include <tonc.h>
/**
* @brief Save game data to SRAM.
*/
void save_game();
/**
* @brief Load game data from SRAM.
*
* @sa save_game
*/
void load_game();
#endif // SAVE_H
+1 -1
View File
@@ -9,4 +9,4 @@
#define TM_ZERO 0
#endif // TIMER_H
#endif // TIMER_H
+11
View File
@@ -0,0 +1,11 @@
/**
* @file version.h
*
* @brief Game ROM version global var declaration.
*/
#ifndef VERSION_H
#define VERSION_H
extern const char gbalatro_version[];
#endif // VERSION_H
+1 -1
View File
@@ -20,7 +20,7 @@ if __name__ == "__main__":
sys.exit(1)
file_path = sys.argv[1]
pattern = "GBALATRO_VERSION"
pattern = "GBALATRO[_,-]VERSION"
for offset, text in grep_binary_offsets(file_path, pattern):
version = text.split(":")[1]
+8 -1
View File
@@ -13,7 +13,7 @@ static IWRAM_CODE void s_affine_background_prep_bgaff_arr();
static BG_AFFINE _bgaff_arr[SCREEN_HEIGHT + 1];
static AFF_SRC_EX _asx = {0};
static enum AffineBackgroundID _background = AFFINE_BG_MAIN_MENU;
static enum AffineBackgroundID _background = AFFINE_BG_NONE;
static uint _timer = 0;
void affine_background_init()
@@ -78,6 +78,11 @@ void affine_background_load_palette(const u16* src)
void affine_background_change_background(enum AffineBackgroundID new_bg)
{
if (_background == new_bg)
{
return;
}
_background = new_bg;
switch (_background)
@@ -110,6 +115,8 @@ void affine_background_change_background(enum AffineBackgroundID new_bg)
GRIT_CPY(&se_mem[AFFINE_BG_SBB], affine_background_gfxMap);
affine_background_load_palette(affine_background_gfxPal);
break;
default:
break;
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
#include "audio_utils.h"
#include "game_variables.h"
#include <maxmod.h>
void play_sfx(mm_word id, mm_word rate, mm_byte volume)
@@ -8,7 +10,7 @@ void play_sfx(mm_word id, mm_word rate, mm_byte volume)
{id},
rate,
0,
volume,
(volume * g_game_vars.sound_volume) / VOLUME_OPTION_MAX,
SFX_DEFAULT_PAN,
};
mmEffectEx(&sfx);
+1 -1
View File
@@ -127,7 +127,7 @@ enum BlindType roll_blind_type(bool showdown)
}
// roll a random blind among the unbeaten ones
int random_blind_idx = rand() % list_get_len(p_unbeaten_blinds);
int random_blind_idx = get_rand() % list_get_len(p_unbeaten_blinds);
Blind* random_blind = list_get_at_idx(p_unbeaten_blinds, random_blind_idx);
return random_blind->type;
+14 -1
View File
@@ -2,6 +2,7 @@
#include "deck_gfx.h"
#include "graphic_utils.h"
#include "high_contrast_deck_pal_gfx.h"
#include <maxmod.h>
#include <stdlib.h>
@@ -21,7 +22,19 @@ const static u16 _card_sprite_lut[NUM_SUITS][NUM_RANKS] = {
void card_init()
{
GRIT_CPY(&pal_obj_mem[CARD_PB], deck_gfxPal);
toggle_high_contrast_cards(false);
}
void toggle_high_contrast_cards(bool enable)
{
if (enable)
{
GRIT_CPY(&pal_obj_mem[CARD_PB], high_contrast_deck_pal_gfxPal);
}
else
{
GRIT_CPY(&pal_obj_mem[CARD_PB], deck_gfxPal);
}
}
// Card methods
+85 -79
View File
@@ -8,16 +8,17 @@
#include "background_main_menu_gfx.h"
#include "background_shop_gfx.h"
#include "bitset.h"
#include "blind.h"
#include "button.h"
#include "card.h"
#include "game/common_ui.h"
#include "game/main_menu.h"
#include "game/options_menu.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "hand_analysis.h"
#include "joker.h"
#include "list.h"
#include "save.h"
#include "selection_grid.h"
#include "soundbank.h"
#include "splash_screen.h"
@@ -64,6 +65,7 @@
#define CARD_UNFOCUSED_SEL_Y 15
#define CARD_FOCUSED_SEL_Y 20
// Timer defs
#define TM_RESET_STATIC_VARS 30
#define TM_END_POP_MENU_ANIM 13
#define TM_START_ROUND_END_REWARDS_ANIM 1
@@ -87,6 +89,7 @@
// TODO: Rename "PID" to "PAL_IDX"
// Palette IDs
#define BOSS_BLIND_PRIMARY_PID 1
#define REROLL_BTN_PID 3
#define BLIND_SKIP_BTN_PID 5
@@ -428,9 +431,6 @@ static const BG_POINT HAND_PLAY_POS = {120, 70};
typedef void (*SubStateActionFn)(void);
// BY DEFAULT IS SET TO 1, but if changed to 2 or more, should speed up all (or most) of the game
// aspects that should be sped up by speed, as in the original game.
static int game_speed = 1;
static enum BackgroundId background = BG_NONE;
static StateInfo state_info[] = {
@@ -552,6 +552,18 @@ static enum PlayState play_state = PLAY_STARTING;
static enum HandType hand_type = NONE;
static ContainedHandTypes _contained_hands = {0};
// Initialization of the global var
// clang-format off
GameVariables g_game_vars = {
0, 0, 0,
0, 0, 0,
DEFAULT_GAME_SPEED,
DEFAULT_HIGH_CONTRAST,
DEFAULT_MUSIC_VOLUME,
DEFAULT_SOUND_VOLUME
};
// clang-format on
// The sprite that displays the blind when in "GAME_PLAYING/GAME_ROUND_END" state
static Sprite* playing_blind_token = NULL;
@@ -586,9 +598,6 @@ static int max_discards = 4;
static int hands = 0;
static int discards = 0;
static int round = 0;
static int ante = 0;
static int money = 0;
static u32 score = 0;
static u32 temp_score = 0; // This is the score that shows in the same spot as the hand type.
static bool score_flames_active = false;
@@ -645,9 +654,6 @@ static int shortcut_joker_count = 0;
static int four_fingers_joker_count = 0;
GBAL_UNUSED
GameVariables g_game_vars;
GBAL_UNUSED
static inline bool is_shop_joker_avail(int joker_id)
{
@@ -729,7 +735,7 @@ static inline void jokers_available_to_shop_init(void)
static void reroll_boss_blind(bool no_tiles)
{
// Showdown blinds only show up on ante 8, 16, etc...
next_boss_blind = roll_blind_type((ante % 8 == 0) && (ante > 0));
next_boss_blind = roll_blind_type((g_game_vars.ante % 8 == 0) && (g_game_vars.ante > 0));
if (!no_tiles)
{
apply_blind_tiles(next_boss_blind, BOSS_BLIND_TOKEN_LAYER);
@@ -781,6 +787,8 @@ void game_init()
jokers_available_to_shop_init();
load_game();
hands = max_hands;
discards = max_discards;
g_game_vars.timer = TM_ZERO;
@@ -788,10 +796,10 @@ void game_init()
blinds_states[0] = BLIND_STATE_CURRENT;
blinds_states[1] = BLIND_STATE_UPCOMING;
blinds_states[2] = BLIND_STATE_UPCOMING;
ante = STARTING_ANTE;
money = STARTING_MONEY;
g_game_vars.ante = STARTING_ANTE;
g_game_vars.money = STARTING_MONEY;
score = STARTING_SCORE;
round = 0;
g_game_vars.round = 0;
// Initialize/reset unbeaten Boss/Showdown Blinds so they are all available
init_unbeaten_blinds_list(false);
@@ -918,6 +926,12 @@ void game_change_state(enum GameState new_game_state)
}
}
u32 get_rand()
{
g_game_vars.rng_step++;
return rand();
}
CardObject** get_hand_array(void)
{
return hand;
@@ -1037,15 +1051,14 @@ int get_num_hands_remaining(void)
return hands;
}
int get_game_speed(void)
enum BlindType get_current_blind()
{
return game_speed;
return current_blind;
}
// for the future when a menu actually lets this variable be changed.
void set_game_speed(int new_game_speed)
enum BlindType get_next_boss_blind()
{
game_speed = new_game_speed;
return next_boss_blind;
}
u32 get_chips(void)
@@ -1068,16 +1081,6 @@ void set_mult(u32 new_mult)
mult = new_mult;
}
int get_money(void)
{
return money;
}
void set_money(int new_money)
{
money = new_money;
}
void set_retrigger(bool new_retrigger)
{
retrigger = new_retrigger;
@@ -1089,7 +1092,7 @@ void display_money()
tte_erase_rect_wrapper(MONEY_TEXT_RECT);
char money_str_buff[INT_MAX_DIGITS + 2]; // + 2 for null terminator and "$" sign
snprintf(money_str_buff, sizeof(money_str_buff), "$%d", money);
snprintf(money_str_buff, sizeof(money_str_buff), "$%d", g_game_vars.money);
// Bias left so the number is centered and the "$" sign is on the left
update_text_rect_to_center_str(&money_text_rect, money_str_buff, SCREEN_LEFT);
@@ -1806,7 +1809,7 @@ static void display_score(u32 value)
static void check_flaming_score(void)
{
u32 curr_score = u32_protected_mult(chips, mult);
u32 required_score = blind_get_requirement(current_blind, ante);
u32 required_score = blind_get_requirement(current_blind, g_game_vars.ante);
if (curr_score >= required_score && !score_flames_active)
{
// start flaming score
@@ -1834,7 +1837,7 @@ static void display_round(void)
ROUND_TEXT_RECT.left,
ROUND_TEXT_RECT.top,
TTE_YELLOW_PB,
round
g_game_vars.round
);
}
@@ -1935,14 +1938,14 @@ static inline void deck_shuffle(void)
{
for (int i = deck_top; i > 0; i--)
{
int j = rand() % (i + 1);
int j = get_rand() % (i + 1);
Card* temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
}
static void game_round_on_init()
static void game_round_on_init(void)
{
hand_state = HAND_DRAW;
cards_drawn = 0;
@@ -1974,7 +1977,7 @@ static void game_round_on_init()
}
Rect blind_req_text_rect = BLIND_REQ_TEXT_RECT;
u32 blind_requirement = blind_get_requirement(current_blind, ante);
u32 blind_requirement = blind_get_requirement(current_blind, g_game_vars.ante);
char blind_req_str_buff[UINT_MAX_DIGITS + 1];
@@ -2019,14 +2022,14 @@ static void game_over_init(void)
main_bg_se_copy_rect(NEW_RUN_BTN_SRC_RECT, NEW_RUN_BTN_DEST_POS);
}
static void game_lose_on_init()
static void game_lose_on_init(void)
{
game_over_init();
// Using the text color to match the "Game Over" text
affine_background_set_color(TEXT_CLR_RED);
}
static void game_win_on_init()
static void game_win_on_init(void)
{
game_over_init();
// Using the text color to match the "You Win" text
@@ -2036,8 +2039,9 @@ static void game_win_on_init()
// General functions
static inline void set_seed(int seed)
{
srand(seed);
g_game_vars.rng_seed = seed;
srand(g_game_vars.rng_seed);
g_game_vars.rng_step = 0;
}
// Playing state functions
@@ -2195,7 +2199,7 @@ static bool game_playing_hand_row_on_selection_changed(
*/
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
}
@@ -2215,7 +2219,7 @@ static bool game_playing_hand_row_on_selection_changed(
*/
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
}
@@ -2377,13 +2381,13 @@ static inline void game_playing_handle_round_over(void)
{
enum GameState next_state = GAME_STATE_ROUND_END;
if (score >= blind_get_requirement(current_blind, ante))
if (score >= blind_get_requirement(current_blind, g_game_vars.ante))
{
if (current_blind > BLIND_TYPE_BIG)
{
if (ante < MAX_ANTE)
if (g_game_vars.ante < MAX_ANTE)
{
display_ante(++ante);
display_ante(++g_game_vars.ante);
// mark current boss blind as beaten and allow for reroll
set_blind_beaten(next_boss_blind);
@@ -2706,7 +2710,7 @@ static bool check_and_score_joker_for_event(
static inline bool game_round_is_over(void)
{
return hands == 0 || score >= blind_get_requirement(current_blind, ante);
return hands == 0 || score >= blind_get_requirement(current_blind, g_game_vars.ante);
}
// Basically a copy of HAND_DISCARD
@@ -3156,7 +3160,7 @@ static inline void game_playing_process_input_and_state(void)
display_chips();
static const int SCORE_CALC_SFX_PITCH_SHIFT = -102; // -10% OF MM_BASE_PITCH_RATE
static const int SCORE_CALC_SFX_VOLUME = 204; // 80% MM_FULL_VOLUME
static const int SCORE_CALC_SFX_VOLUME = 204; // 80% MM_SFX_FULL_VOLUME
// The chips calculation SFX is the same as button
play_sfx(
@@ -3171,10 +3175,10 @@ static inline void game_playing_process_input_and_state(void)
/* Using fixed point in case the score is lower than NUM_SCORE_LERP_STEPS and then
* then the division rounds it down to 0 and it's never added to the total.
* The operation is equivalent to
* fxdiv(int2fx(temp_score * get_game_speed()), int2fx(NUM_SCORE_LERP_STEPS))
* fxdiv(int2fx(temp_score * g_game_vars.game_speed), int2fx(NUM_SCORE_LERP_STEPS))
*/
lerped_temp_score -= int2fx(temp_score * get_game_speed()) / NUM_SCORE_LERP_STEPS;
lerped_score += int2fx(temp_score * get_game_speed()) / NUM_SCORE_LERP_STEPS;
lerped_temp_score -= int2fx(temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS;
lerped_score += int2fx(temp_score * g_game_vars.game_speed) / NUM_SCORE_LERP_STEPS;
if (lerped_temp_score > 0)
{
@@ -3504,7 +3508,7 @@ static inline void game_playing_process_flaming_score(void)
}
}
static void game_playing_on_update()
static void game_playing_on_update(void)
{
// Background logic (thissss might be moved to the card'ssss logic later. I'm a sssssnake)
if (hand_state == HAND_DRAW || hand_state == HAND_DISCARD || hand_state == HAND_SELECT)
@@ -3537,13 +3541,13 @@ static void game_playing_on_update()
static int calculate_interest_reward(void)
{
int reward = (money / 5) * INTEREST_PER_5;
int reward = (g_game_vars.money / 5) * INTEREST_PER_5;
if (reward > MAX_INTEREST)
reward = MAX_INTEREST;
return reward;
}
static void game_round_end_on_exit()
static void game_round_end_on_exit(void)
{
// Cleanup blind tokens from this round to avoid accumulating
// allocated blind sprites each round
@@ -3555,7 +3559,7 @@ static void game_round_end_on_exit()
// TODO: Reuse sprites for blind selection?
}
static void game_round_end_on_update()
static void game_round_end_on_update(void)
{
if (state_info[game_state].substate == ROUND_END_EXIT)
{
@@ -3606,7 +3610,7 @@ static void game_round_end_display_finished_blind()
{
obj_unhide(round_end_blind_token->obj, 0);
int current_ante = ante;
int current_ante = g_game_vars.ante;
// Beating the boss blind increases the ante, so we need to display the previous ante value
if (current_blind > BLIND_TYPE_BIG)
@@ -3854,7 +3858,7 @@ static void game_round_end_display_rewards()
static inline void game_round_end_cashout(void)
{
// Reward the player
money += hands + blind_get_reward(current_blind) + calculate_interest_reward();
g_game_vars.money += hands + blind_get_reward(current_blind) + calculate_interest_reward();
display_money();
hands = max_hands; // Reset the hands to the maximum
@@ -3969,7 +3973,7 @@ static inline int game_shop_get_rand_available_joker_id(void)
return UNDEFINED;
int matching_joker_ids[jokers_avail_size];
int fallback_random_idx = random() % jokers_avail_size;
int fallback_random_idx = get_rand() % jokers_avail_size;
int fallback_random_joker_id = UNDEFINED;
int match_count = 0;
@@ -3989,7 +3993,7 @@ static inline int game_shop_get_rand_available_joker_id(void)
}
int selected_joker_id =
(match_count > 0) ? matching_joker_ids[random() % match_count] : fallback_random_joker_id;
(match_count > 0) ? matching_joker_ids[get_rand() % match_count] : fallback_random_joker_id;
return selected_joker_id;
}
@@ -4160,7 +4164,7 @@ static inline void game_sell_joker(int joker_idx)
return;
JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_owned_jokers_list, joker_idx);
money += joker_get_sell_value(joker_object->joker);
g_game_vars.money += joker_get_sell_value(joker_object->joker);
display_money();
erase_price_under_sprite_object(joker_object->sprite_object);
@@ -4216,8 +4220,8 @@ static inline void game_shop_buy_joker(int shop_joker_idx)
{
JokerObject* joker_object = (JokerObject*)list_get_at_idx(&_shop_jokers_list, shop_joker_idx);
money -= joker_object->joker->value; // Deduct the money spent on the joker
display_money(); // Update the money display
g_game_vars.money -= joker_object->joker->value; // Deduct the money spent on the joker
display_money(); // Update the money display
erase_price_under_sprite_object(joker_object->sprite_object);
sprite_object_set_focus(joker_object->sprite_object, false);
add_to_held_jokers(joker_object);
@@ -4255,7 +4259,7 @@ static void shop_top_row_on_key_transit(SelectionGrid* selection_grid, Selection
JokerObject* joker_object =
(JokerObject*)list_get_at_idx(&_shop_jokers_list, shop_joker_idx);
if (joker_object == NULL || list_get_len(&_owned_jokers_list) >= MAX_JOKERS_HELD_SIZE ||
money < joker_object->joker->value)
g_game_vars.money < joker_object->joker->value)
{
return;
}
@@ -4354,7 +4358,7 @@ static bool shop_reroll_row_on_selection_changed(
static inline void game_shop_reroll(int* reroll_cost)
{
money -= *reroll_cost;
g_game_vars.money -= *reroll_cost;
display_money(); // Update the money display
ListItr itr = list_itr_create(&_shop_jokers_list);
@@ -4405,7 +4409,7 @@ static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Select
return;
}
if (money >= reroll_cost)
if (g_game_vars.money >= reroll_cost)
{
// TODO: Add money sound effect
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
@@ -4503,7 +4507,7 @@ static inline void game_shop_lights_anim_frame(void)
memcpy16(&pal_bg_mem[SHOP_LIGHTS_1_PID], &shifted_palette[3], 1);
}
static void game_shop_on_update()
static void game_shop_on_update(void)
{
change_background(BG_SHOP);
@@ -4536,7 +4540,7 @@ static void game_shop_on_update()
shop_state_actions[substate]();
}
static void game_shop_on_exit()
static void game_shop_on_exit(void)
{
ListItr itr = list_itr_create(&_shop_jokers_list);
JokerObject* joker_object;
@@ -4554,9 +4558,11 @@ static void game_shop_on_exit()
list_clear(&_shop_jokers_list);
increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()?
save_game();
}
static void game_blind_select_on_init()
static void game_blind_select_on_init(void)
{
change_background(BG_BLIND_SELECT);
selection_x = 0;
@@ -4565,7 +4571,7 @@ static void game_blind_select_on_init()
play_sfx(SFX_POP, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
}
static void game_blind_select_on_update()
static void game_blind_select_on_update(void)
{
if (state_info[game_state].substate == BLIND_SELECT_MAX)
{
@@ -4638,7 +4644,7 @@ static inline void game_blind_select_print_blind_req(enum BlindTokens blind)
{
Rect blind_req_score_rect = game_blind_select_get_req_score_rect(blind);
u32 blind_req = blind_get_requirement(get_blind_type_from_token(blind), ante);
u32 blind_req = blind_get_requirement(get_blind_type_from_token(blind), g_game_vars.ante);
char blind_req_str_buff[UINT_MAX_DIGITS + 1];
truncate_uint_to_suffixed_str(
@@ -4736,7 +4742,7 @@ static void game_blind_select_handle_input()
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
state_info[game_state].substate = BLIND_SELECTED_ANIM_SEQ;
g_game_vars.timer = TM_ZERO;
++round;
++g_game_vars.round;
display_round();
}
else if (current_blind <= BLIND_TYPE_BIG)
@@ -4853,25 +4859,25 @@ static void game_blind_select_display_blind_panel()
}
}
static void game_blind_select_on_exit()
static void game_blind_select_on_exit(void)
{
selection_y = 0;
background = UNDEFINED;
}
void game_start()
void game_start(void)
{
set_seed(g_game_vars.rng_seed);
// set_seed(9); // 9 is a full house
set_seed(g_game_vars.rng_seed);
affine_background_change_background(AFFINE_BG_GAME);
// Normally I would just cache these and hide/unhide but I didn't feel like dealing with
// defining a layer for it
hands = max_hands;
discards = max_discards;
// Activate high contrast palette for cards if loaded settings tell us to
toggle_high_contrast_cards(g_game_vars.high_contrast);
// Fill the deck with all the cards. Later on this can be replaced with a more dynamic system
// that allows for different decks and card types.
for (int suit = 0; suit < NUM_SUITS; suit++)
@@ -4912,7 +4918,7 @@ void game_start()
ANTE_TEXT_RECT.left,
ANTE_TEXT_RECT.top,
TTE_YELLOW_PB,
ante,
g_game_vars.ante,
TTE_WHITE_PB,
MAX_ANTE
); // Ante
@@ -4934,7 +4940,7 @@ static inline void game_over_process_user_input()
}
}
static void game_lose_on_update()
static void game_lose_on_update(void)
{
if (g_game_vars.timer < GAME_OVER_ANIM_FRAMES)
{
@@ -4956,7 +4962,7 @@ static void game_lose_on_update()
// This function isn't set in stone. This is just a placeholder
// allowing the player to restart the game. Thought it would be nice to have
// util we decide what we want to do after a game over.
static void game_over_on_exit()
static void game_over_on_exit(void)
{
while (list_get_len(&_owned_jokers_list) > 0)
{
@@ -4996,7 +5002,7 @@ static void game_over_on_exit()
ANTE_TEXT_RECT.left,
ANTE_TEXT_RECT.top,
TTE_YELLOW_PB,
ante,
g_game_vars.ante,
TTE_WHITE_PB,
MAX_ANTE
);
@@ -5004,7 +5010,7 @@ static void game_over_on_exit()
affine_background_load_palette(affine_background_gfxPal);
}
static void game_win_on_update()
static void game_win_on_update(void)
{
if (g_game_vars.timer < GAME_OVER_ANIM_FRAMES)
{
+7 -6
View File
@@ -2,6 +2,7 @@
#include "game.h"
#include "game/main_menu.h"
#include "game/options_menu.h"
typedef void (*BackgroundRenderCallback)(void);
@@ -15,18 +16,18 @@ static const BackgroundRenderCallback bgCallbacks[] = {
[BG_ROUND_END] = NULL,
[BG_SHOP] = NULL,
[BG_BLIND_SELECT] = NULL,
[BG_OPTIONS_MENU] = game_options_menu_change_background,
[BG_MAIN_MENU] = game_main_menu_change_background,
};
void change_background(enum BackgroundId id)
{
if (id == BG_MAIN_MENU)
if (id != background && bgCallbacks[id] != NULL)
{
if (id != background)
{
bgCallbacks[id]();
}
bgCallbacks[id]();
}
change_background_legacy(id);
background = id;
// Can be removed once all states have their own "change_background" func
change_background_legacy(id);
}
+152 -41
View File
@@ -9,29 +9,79 @@
#include "game/common_ui.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "selection_grid.h"
#include "soundbank.h"
#include "sprite.h"
#include "version.h"
#include <stdint.h>
#include <tonc.h>
#include <tonc_math.h>
#include <tonc_memdef.h>
static const u32 PLAY_BUTTON_OUTLINE_PID = 2;
static const u32 PLAY_BUTTON_MAIN_COLOR_PID = 5;
#define PLAY_BUTTON_MAIN_COLOR_PID 5
#define PLAY_BUTTON_OUTLINE_PID 6
#define OPTIONS_BUTTON_MAIN_COLOR_PID 7
#define OPTIONS_BUTTON_OUTLINE_PID 1
static const u32 IMPLEMENTED_BUTTONS = 1; // Remove this once all buttons are implemented
static const u32 PLAY_BTN_IDX = 0;
enum MainButtons
{
PLAY_BTN_IDX,
OPTIONS_BTN_IDX,
MAIN_MENU_NB_BTN
};
// Pixel sizes
#define MAIN_MENU_ACE_T_X 88
#define MAIN_MENU_ACE_T_Y 26
// clang-format off
static const BG_POINT GBALATRO_VERSION_TEXT_POS = {0, 152};
static const Rect GBALATRO_VERSION_TEXT_RECT = {0, 152, 240, 160};
// clang-format on
// Define SelectionGrid for the main menu buttons
static int main_menu_return_row_size(void);
static bool main_menu_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static void play_on_pressed(void);
static void options_on_pressed(void);
// clang-format off
static SelectionGridRow main_menu_selection_rows[] = {
{
0,
main_menu_return_row_size,
main_menu_on_selection_changed,
main_menu_on_key_transit,
{.wrap = false}
},
};
static Button main_menu_buttons[] = {
{PLAY_BUTTON_OUTLINE_PID, PLAY_BUTTON_MAIN_COLOR_PID, play_on_pressed, NULL},
{OPTIONS_BUTTON_OUTLINE_PID, OPTIONS_BUTTON_MAIN_COLOR_PID, options_on_pressed, NULL},
};
static SelectionGrid main_menu_selection_grid = {
main_menu_selection_rows,
1,
{0, 0}
};
// clang-format on
// Main menu sprite - the ace of spades
static CardObject* main_menu_ace = NULL;
// Current selected button index
static int selection_x = 0;
// Keep track of last highlighted button
static enum MainButtons last_highlighted_button = PLAY_BTN_IDX;
void game_main_menu_change_background(void)
{
@@ -42,11 +92,14 @@ void game_main_menu_change_background(void)
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_main_menu_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_main_menu_gfxMap);
// Disable the button highlight colors
memcpy16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], &pal_bg_mem[PLAY_BUTTON_MAIN_COLOR_PID], 1);
// Disable all button highlight colors
for (int i = 0; i < MAIN_MENU_NB_BTN; i++)
{
button_set_highlight(&main_menu_buttons[i], false);
}
}
void game_main_menu_on_init()
void game_main_menu_on_init(void)
{
affine_background_change_background(AFFINE_BG_MAIN_MENU);
change_background(BG_MAIN_MENU);
@@ -58,10 +111,18 @@ void game_main_menu_on_init()
main_menu_ace->sprite_object->ty = int2fx(MAIN_MENU_ACE_T_Y);
main_menu_ace->sprite_object->y = main_menu_ace->sprite_object->ty;
main_menu_ace->sprite_object->tscale = float2fx(0.8f);
selection_x = 0;
card_object_update(main_menu_ace);
// Select last highlighted button, Play button by default.
// e.g. if we return from the options menu, we want the Options button to be highlighted.
Selection sel_init = {last_highlighted_button, 0};
main_menu_selection_grid.selection = sel_init;
// Highlight current button
button_set_highlight(&main_menu_buttons[main_menu_selection_grid.selection.x], true);
}
void game_main_menu_on_update()
void game_main_menu_on_update(void)
{
card_object_update(main_menu_ace);
main_menu_ace->sprite_object->trotation = lu_sin((g_game_vars.timer << 8) / 2) / 3;
@@ -75,41 +136,91 @@ void game_main_menu_on_update()
g_game_vars.rng_seed *= 2;
}
if (key_hit(KEY_LEFT))
{
if (selection_x > 0)
{
selection_x--;
}
}
else if (key_hit(KEY_RIGHT))
{
if (selection_x < IMPLEMENTED_BUTTONS - 1)
{
selection_x++;
}
}
if (selection_x == PLAY_BTN_IDX)
{
memset16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], BTN_HIGHLIGHT_COLOR, 1);
if (key_hit(SELECT_CARD))
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
game_change_state(GAME_STATE_GAME_START);
}
}
else
{
memcpy16(&pal_bg_mem[PLAY_BUTTON_OUTLINE_PID], &pal_bg_mem[PLAY_BUTTON_MAIN_COLOR_PID], 1);
}
selection_grid_process_input(&main_menu_selection_grid);
}
void game_main_menu_on_exit()
void game_main_menu_on_exit(void)
{
// Save selected button
last_highlighted_button = main_menu_selection_grid.selection.x;
// Normally I would just cache these and hide/unhide but I didn't feel like dealing with
// defining a layer for it
card_destroy(&main_menu_ace->card);
card_object_destroy(&main_menu_ace);
}
// Implement SelectionGrid handler functions
static int main_menu_return_row_size(void)
{
return MAIN_MENU_NB_BTN;
}
static bool main_menu_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
if (prev_selection->x >= 0 && prev_selection->x < MAIN_MENU_NB_BTN)
{
button_set_highlight(&main_menu_buttons[prev_selection->x], false);
}
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
button_set_highlight(&main_menu_buttons[new_selection->x], true);
return true;
}
static void main_menu_on_key_transit(SelectionGrid* selection_grid, Selection* selection)
{
// DEBUG: toggle git hash display by pressing L+R+START+SELECT
static bool combo_pressed = false;
static bool show_version = false;
if (key_is_down(KEY_START) && key_is_down(KEY_SELECT) && key_is_down(KEY_L) &&
key_is_down(KEY_R))
{
if (!combo_pressed)
{
show_version = !show_version;
if (show_version)
{
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
GBALATRO_VERSION_TEXT_POS.x,
GBALATRO_VERSION_TEXT_POS.y,
TTE_WHITE_PB,
gbalatro_version
);
}
else
{
tte_erase_rect_wrapper(GBALATRO_VERSION_TEXT_RECT);
}
}
combo_pressed = true;
}
else
{
combo_pressed = false;
}
if (key_hit(SELECT_CARD))
{
button_press(&main_menu_buttons[selection->x]);
}
}
static void play_on_pressed(void)
{
game_change_state(GAME_STATE_GAME_START);
}
static void options_on_pressed(void)
{
game_change_state(GAME_STATE_OPTIONS_MENU);
}
+624
View File
@@ -0,0 +1,624 @@
#include "game/options_menu.h"
#include "affine_background.h"
#include "audio_utils.h"
#include "background_options_menu_gfx.h"
#include "button.h"
#include "game.h"
#include "game/common_ui.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "save.h"
#include "selection_grid.h"
#include "soundbank.h"
#include "util.h"
#include <stdint.h>
#include <string.h>
#include <tonc.h>
#include <tonc_math.h>
#include <tonc_memdef.h>
// Button indices
enum OptionButtons
{
GAME_SPEED_BTN_IDX,
HIGH_CONTRAST_BTN_IDX,
MUSIC_VOLUME_BTN_IDX,
SOUND_VOLUME_BTN_IDX,
BACK_BTN_IDX,
NB_OPTIONS_BUTTONS
};
enum OptionSpeedButtons
{
GAME_SPEED_DOWN_BTN_IDX,
GAME_SPEED_UP_BTN_IDX,
NB_GAME_SPEED_BUTTONS
};
// Color palette indices
#define MENU_BUTTON_MAIN_COLOR_PAL_IDX 1
#define BACK_BUTTON_MAIN_COLOR_PAL_IDX 3
#define SPEED_DOWN_BUTTON_OUTLINE_COLOR_PAL_IDX 4
#define SPEED_BUTTON_OUTLINE_COLOR_PAL_IDX 5
#define SPEED_UP_BUTTON_OUTLINE_COLOR_PAL_IDX 6
#define CONTRAST_BUTTON_OUTLINE_COLOR_PAL_IDX 7
#define MUSIC_BUTTON_OUTLINE_COLOR_PAL_IDX 8
#define SOUND_BUTTON_OUTLINE_COLOR_PAL_IDX 9
#define BACK_BUTTON_OUTLINE_COLOR_PAL_IDX 10
// Define selection grid for the menu buttons
static void game_speed_down_on_pressed(void);
static void game_speed_up_on_pressed(void);
static void high_contrast_on_pressed(void);
static void back_on_pressed(void);
static int options_menu_return_row_size(void);
static void options_menu_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static bool game_speed_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static bool music_volume_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static bool sound_volume_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static bool regular_button_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
// clang-format off
static SelectionGridRow options_menu_selection_rows[] = {
{
GAME_SPEED_BTN_IDX,
options_menu_return_row_size,
game_speed_row_on_selection_changed,
NULL,
{.wrap = true} // The wrapping is used to allow button activation with Left/Right arrows by
// putting logic into the `game_speed_row_on_selection_changed` function
},
{
HIGH_CONTRAST_BTN_IDX,
options_menu_return_row_size,
regular_button_row_on_selection_changed,
options_menu_row_on_key_transit,
{.wrap = false}
},
{
MUSIC_VOLUME_BTN_IDX,
options_menu_return_row_size,
music_volume_row_on_selection_changed,
NULL,
{.wrap = true} // Same trick as Game Speed button
},
{
SOUND_VOLUME_BTN_IDX,
options_menu_return_row_size,
sound_volume_row_on_selection_changed,
NULL,
{.wrap = true} // Same trick as Game Speed button
},
{
BACK_BTN_IDX,
options_menu_return_row_size,
regular_button_row_on_selection_changed,
options_menu_row_on_key_transit,
{.wrap = false}
}
};
static Button options_menu_buttons[] = {
{SPEED_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL},
{CONTRAST_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, high_contrast_on_pressed, NULL},
{MUSIC_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL},
{SOUND_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, NULL, NULL},
{BACK_BUTTON_OUTLINE_COLOR_PAL_IDX, BACK_BUTTON_MAIN_COLOR_PAL_IDX, back_on_pressed, NULL}
};
static Button game_speed_buttons[] = {
{SPEED_DOWN_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, game_speed_down_on_pressed, NULL},
{SPEED_UP_BUTTON_OUTLINE_COLOR_PAL_IDX, MENU_BUTTON_MAIN_COLOR_PAL_IDX, game_speed_up_on_pressed, NULL},
};
// clang-format on
const Selection OPTIONS_MENU_INIT_SEL = {0, 0};
static SelectionGrid options_menu_selection_grid = {
options_menu_selection_rows,
NB_OPTIONS_BUTTONS,
OPTIONS_MENU_INIT_SEL
};
// Positions/Rects used to construct and update the menu
// clang-format off
// Values in tiles
static const Rect OPTIONS_SPEED_DOWN_ACTIVE_BTN_SRC_RECT = { 3, 20, 3, 21};
static const Rect OPTIONS_SPEED_DOWN_DISABLED_BTN_SRC_RECT = { 9, 22, 9, 23};
static const BG_POINT OPTIONS_SPEED_DOWN_BTN_DEST_POS = {11, 3};
static const Rect OPTIONS_SPEED_UP_ACTIVE_BTN_SRC_RECT = {10, 20, 10, 21};
static const Rect OPTIONS_SPEED_UP_DISABLED_BTN_SRC_RECT = {10, 22, 10, 23};
static const BG_POINT OPTIONS_SPEED_UP_BTN_DEST_POS = {18, 3};
static const Rect OPTIONS_SPEED_VALUES[GAME_SPEED_MAX] = { { 6, 20, 7, 21},
{ 3, 22, 4, 23},
{ 5, 22, 6, 23},
{ 7, 22, 8, 23} };
static const BG_POINT OPTIONS_SPEED_VALUE_DEST_POS = {14, 3};
static const Rect OPTIONS_CONTRAST_VALUE_YES_SRC_RECT = { 4, 24, 7, 25};
static const Rect OPTIONS_CONTRAST_VALUE_NO_SRC_RECT = { 9, 24, 12, 25};
static const BG_POINT OPTIONS_CONTRAST_VALUE_DEST_POS = {13, 7};
static const Rect OPTIONS_MUSIC_SLIDER_FULL_SRC = {12, 20, 12, 20};
static const Rect OPTIONS_MUSIC_SLIDER_MID_SRC = {13, 20, 13, 20};
static const Rect OPTIONS_MUSIC_SLIDER_EMPTY_SRC = {14, 20, 14, 20};
static const Rect OPTIONS_SOUND_SLIDER_FULL_SRC = {12, 22, 12, 22};
static const Rect OPTIONS_SOUND_SLIDER_MID_SRC = {13, 22, 13, 22};
static const Rect OPTIONS_SOUND_SLIDER_EMPTY_SRC = {14, 22, 14, 22};
static const BG_POINT OPTIONS_MUSIC_SLIDER_START_POS = { 5, 11};
static const BG_POINT OPTIONS_SOUND_SLIDER_START_POS = { 5, 14};
static const BG_POINT OPTIONS_MUSIC_SLIDER_END_POS = {24, 11};
static const BG_POINT OPTIONS_SOUND_SLIDER_END_POS = {24, 14};
static const u8 OPTIONS_MUSIC_SLIDER_SEGMENT_LENGTH = (OPTIONS_MUSIC_SLIDER_END_POS.x - OPTIONS_MUSIC_SLIDER_START_POS.x + 1)
/ VOLUME_OPTION_MAX;
static const u8 OPTIONS_SOUND_SLIDER_SEGMENT_LENGTH = (OPTIONS_SOUND_SLIDER_END_POS.x - OPTIONS_SOUND_SLIDER_START_POS.x + 1)
/ VOLUME_OPTION_MAX;
// Values in pixels
static const BG_POINT OPTIONS_GAME_SPEED_TEXT_POS = { 82, 16};
static const BG_POINT OPTIONS_HIGH_CONTRAST_TEXT_POS = { 40, 48};
static const BG_POINT OPTIONS_MUSIC_VOLUME_TEXT_POS = { 56, 80};
static const BG_POINT OPTIONS_MUSIC_VALUE_TEXT_POS = {160, 80};
static const BG_POINT OPTIONS_SOUND_VOLUME_TEXT_POS = { 56, 104};
static const BG_POINT OPTIONS_SOUND_VALUE_TEXT_POS = {160, 104};
static const BG_POINT OPTIONS_BACK_SAVE_TEXT_POS = { 72, 136};
// clang-format on
#define GAME_SPEED_ARROW_HIGHLIGHT_DURATION 10
enum OptionSpeedButtons game_speed_arrow_highlight_button = GAME_SPEED_UP_BTN_IDX;
static s32 game_speed_arrow_highlight_start = UNDEFINED;
static bool any_value_changed = false;
static bool back_btn_is_save_state = false;
static void disable_all_game_speed_outlines_except_self(enum OptionSpeedButtons highlighted_btn)
{
for (int i = 0; i < NB_GAME_SPEED_BUTTONS; i++)
{
button_set_highlight(&game_speed_buttons[i], i == highlighted_btn);
}
}
static void disable_all_outlines_except_self(enum OptionButtons highlighted_btn)
{
// These two get disabled no matter what here
disable_all_game_speed_outlines_except_self(NB_GAME_SPEED_BUTTONS);
for (int i = 0; i < NB_OPTIONS_BUTTONS; i++)
{
button_set_highlight(&options_menu_buttons[i], i == highlighted_btn);
}
}
static void update_game_speed_button_graphics()
{
// check if need to disable game speed arrows
if (g_game_vars.game_speed == GAME_SPEED_MIN)
{
main_bg_se_copy_rect(
OPTIONS_SPEED_DOWN_DISABLED_BTN_SRC_RECT,
OPTIONS_SPEED_DOWN_BTN_DEST_POS
);
}
else
{
main_bg_se_copy_rect(
OPTIONS_SPEED_DOWN_ACTIVE_BTN_SRC_RECT,
OPTIONS_SPEED_DOWN_BTN_DEST_POS
);
}
if (g_game_vars.game_speed == GAME_SPEED_MAX)
{
main_bg_se_copy_rect(OPTIONS_SPEED_UP_DISABLED_BTN_SRC_RECT, OPTIONS_SPEED_UP_BTN_DEST_POS);
}
else
{
main_bg_se_copy_rect(OPTIONS_SPEED_UP_ACTIVE_BTN_SRC_RECT, OPTIONS_SPEED_UP_BTN_DEST_POS);
}
main_bg_se_copy_rect(
OPTIONS_SPEED_VALUES[g_game_vars.game_speed - 1],
OPTIONS_SPEED_VALUE_DEST_POS
);
any_value_changed = true;
}
static void update_high_contrast_button_graphics()
{
if (g_game_vars.high_contrast)
{
main_bg_se_copy_rect(OPTIONS_CONTRAST_VALUE_YES_SRC_RECT, OPTIONS_CONTRAST_VALUE_DEST_POS);
}
else
{
main_bg_se_copy_rect(OPTIONS_CONTRAST_VALUE_NO_SRC_RECT, OPTIONS_CONTRAST_VALUE_DEST_POS);
}
any_value_changed = true;
}
static void update_volume_slider_graphics(enum OptionButtons slider_idx)
{
u8 slider_value;
BG_POINT slider_segment_dest;
int slider_segment_length;
Rect slider_segment_full;
Rect slider_segment_mid;
Rect slider_segment_empty;
BG_POINT slider_text_pos;
switch (slider_idx)
{
case MUSIC_VOLUME_BTN_IDX:
slider_value = g_game_vars.music_volume;
slider_segment_dest = OPTIONS_MUSIC_SLIDER_START_POS;
slider_segment_length = OPTIONS_MUSIC_SLIDER_SEGMENT_LENGTH;
slider_segment_full = OPTIONS_MUSIC_SLIDER_FULL_SRC;
slider_segment_mid = OPTIONS_MUSIC_SLIDER_MID_SRC;
slider_segment_empty = OPTIONS_MUSIC_SLIDER_EMPTY_SRC;
slider_text_pos = OPTIONS_MUSIC_VALUE_TEXT_POS;
break;
case SOUND_VOLUME_BTN_IDX:
slider_value = g_game_vars.sound_volume;
slider_segment_dest = OPTIONS_SOUND_SLIDER_START_POS;
slider_segment_length = OPTIONS_SOUND_SLIDER_SEGMENT_LENGTH;
slider_segment_full = OPTIONS_SOUND_SLIDER_FULL_SRC;
slider_segment_mid = OPTIONS_SOUND_SLIDER_MID_SRC;
slider_segment_empty = OPTIONS_SOUND_SLIDER_EMPTY_SRC;
slider_text_pos = OPTIONS_SOUND_VALUE_TEXT_POS;
break;
// This function only takes in a volume slider
default:
return;
}
int i = 0;
// full part of the bar
for (; i < slider_value * slider_segment_length - 1; i++)
{
main_bg_se_copy_rect(slider_segment_full, slider_segment_dest);
slider_segment_dest.x++;
}
// at exactly `slider_value` we either:
// - are in the middle of the bar, then we draw the frontier between full and empty tiles
// - are at the end, then draw a full segment
if (slider_value == VOLUME_OPTION_MAX)
{
main_bg_se_copy_rect(slider_segment_full, slider_segment_dest);
}
else
{
if (slider_value != VOLUME_OPTION_MIN)
{
// draw middle point
main_bg_se_copy_rect(slider_segment_mid, slider_segment_dest);
i++;
slider_segment_dest.x++;
}
// empty part of the bar
for (; i < VOLUME_OPTION_MAX * slider_segment_length; i++)
{
main_bg_se_copy_rect(slider_segment_empty, slider_segment_dest);
slider_segment_dest.x++;
}
}
tte_printf(
"#{P:%d,%d; cx:0x%X000}%3d",
slider_text_pos.x,
slider_text_pos.y,
TTE_WHITE_PB,
(slider_value * VOLUME_OPTION_INCREMENT)
);
any_value_changed = true;
}
static void change_back_save_text(bool is_back)
{
char* btn_text = is_back ? " Back " : "Save Changes";
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
OPTIONS_BACK_SAVE_TEXT_POS.x,
OPTIONS_BACK_SAVE_TEXT_POS.y,
TTE_WHITE_PB,
btn_text
);
}
void game_options_menu_change_background(void)
{
tte_erase_screen();
CBB_CLEAR(MAIN_BG_CBB);
GRIT_CPY(pal_bg_mem, background_options_menu_gfxPal);
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_options_menu_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_options_menu_gfxMap);
tte_printf(
"#{P:%d,%d; cx:0x%X000}Game Speed",
OPTIONS_GAME_SPEED_TEXT_POS.x,
OPTIONS_GAME_SPEED_TEXT_POS.y,
TTE_WHITE_PB
);
tte_printf(
"#{P:%d,%d; cx:0x%X000}High Contrast Cards",
OPTIONS_HIGH_CONTRAST_TEXT_POS.x,
OPTIONS_HIGH_CONTRAST_TEXT_POS.y,
TTE_WHITE_PB
);
tte_printf(
"#{P:%d,%d; cx:0x%X000}Music Volume",
OPTIONS_MUSIC_VOLUME_TEXT_POS.x,
OPTIONS_MUSIC_VOLUME_TEXT_POS.y,
TTE_WHITE_PB
);
tte_printf(
"#{P:%d,%d; cx:0x%X000}Sound Volume",
OPTIONS_SOUND_VOLUME_TEXT_POS.x,
OPTIONS_SOUND_VOLUME_TEXT_POS.y,
TTE_WHITE_PB
);
change_back_save_text(true);
}
void game_options_menu_on_init(void)
{
change_background(BG_OPTIONS_MENU);
// Select game speed button by default
options_menu_selection_grid.selection = OPTIONS_MENU_INIT_SEL;
disable_all_outlines_except_self(GAME_SPEED_BTN_IDX);
// Do an update on the first frame
update_game_speed_button_graphics();
update_high_contrast_button_graphics();
update_volume_slider_graphics(MUSIC_VOLUME_BTN_IDX);
update_volume_slider_graphics(SOUND_VOLUME_BTN_IDX);
// Prevent "Back" button to become "Save Changes" when we first load the data,
// the player hasn't technically changed anything so it would be confusing.
any_value_changed = false;
}
void game_options_menu_on_update(void)
{
selection_grid_process_input(&options_menu_selection_grid);
// game speed arrows small animation: they stay highlighted for a few frames
if (game_speed_arrow_highlight_start != UNDEFINED &&
(g_game_vars.timer - game_speed_arrow_highlight_start) >
GAME_SPEED_ARROW_HIGHLIGHT_DURATION)
{
game_speed_arrow_highlight_start = UNDEFINED;
button_set_highlight(&game_speed_buttons[game_speed_arrow_highlight_button], false);
}
// Except on the first update where it is forced to display the loaded values
// instead of the default ones, if any value was changed, edit the last button
// text to read "Save Changes" instead of "Back"
if (any_value_changed && !back_btn_is_save_state)
{
change_back_save_text(false);
back_btn_is_save_state = true;
}
any_value_changed = false;
}
void game_options_menu_on_exit(void)
{
tte_erase_screen();
}
static void game_speed_down_on_pressed(void)
{
g_game_vars.game_speed--;
game_speed_arrow_highlight_start = g_game_vars.timer;
game_speed_arrow_highlight_button = GAME_SPEED_DOWN_BTN_IDX;
disable_all_game_speed_outlines_except_self(GAME_SPEED_DOWN_BTN_IDX);
update_game_speed_button_graphics();
}
static void game_speed_up_on_pressed(void)
{
g_game_vars.game_speed++;
game_speed_arrow_highlight_start = g_game_vars.timer;
game_speed_arrow_highlight_button = GAME_SPEED_UP_BTN_IDX;
disable_all_game_speed_outlines_except_self(GAME_SPEED_UP_BTN_IDX);
update_game_speed_button_graphics();
}
/**
* @brief Handles input for the high contrast card toggle button and nothing more.
*/
static void high_contrast_on_pressed(void)
{
g_game_vars.high_contrast = (g_game_vars.high_contrast == 1) ? false : true;
update_high_contrast_button_graphics();
}
/**
* @brief Handles input for the Back/Save button.
*/
static void back_on_pressed(void)
{
if (back_btn_is_save_state)
{
save_game();
change_back_save_text(true);
back_btn_is_save_state = false;
}
else
{
game_change_state(GAME_STATE_MAIN_MENU);
}
}
/**
* @brief Gives the width of options menu rows in selection grid.
*
* @returns Always 1, all rows contain only 1 button. Could have been a define
* but I had to make this a function so it could be given to the
* SelectionGrid constructor.
*/
static int options_menu_return_row_size(void)
{
return 1;
}
static void change_button_highlight(
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
if (prev_selection->y == row_idx && prev_selection->x >= 0 &&
prev_selection->x < NB_OPTIONS_BUTTONS)
{
button_set_highlight(&options_menu_buttons[row_idx], false);
}
if (new_selection->y == row_idx)
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
button_set_highlight(&options_menu_buttons[row_idx], true);
}
}
/**
* @brief Handles left/Right inputs on the row of the Game Speed button.
* Is used to increased/decrease the speed value, instead of relying on a
* `on_key_transit` function because the latter does not take directional
* inputs.
*/
static bool game_speed_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
change_button_highlight(row_idx, prev_selection, new_selection);
disable_all_game_speed_outlines_except_self(NB_GAME_SPEED_BUTTONS);
// Can only change game speed by pressing left/right while staying on the button's row
if (prev_selection->y != new_selection->y)
return true;
if (key_hit(KEY_LEFT) && g_game_vars.game_speed > GAME_SPEED_MIN)
{
button_press(&game_speed_buttons[GAME_SPEED_DOWN_BTN_IDX]);
}
else if (key_hit(KEY_RIGHT) && g_game_vars.game_speed < GAME_SPEED_MAX)
{
button_press(&game_speed_buttons[GAME_SPEED_UP_BTN_IDX]);
}
return true;
}
/**
* @brief Handles input for all SelectionGridRows in options menu, only actually does
* something on rows containing "normal" buttons (those for which the on_pressed
* method in options_menu_buttons is not NULL).
*/
static void options_menu_row_on_key_transit(SelectionGrid* selection_grid, Selection* selection)
{
if (key_hit(SELECT_CARD))
{
button_press(&options_menu_buttons[selection->y]);
}
}
static bool regular_button_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
change_button_highlight(row_idx, prev_selection, new_selection);
return true;
}
static bool music_volume_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
change_button_highlight(row_idx, prev_selection, new_selection);
if (prev_selection->y != new_selection->y)
return true;
if (key_hit(KEY_LEFT) && g_game_vars.music_volume > VOLUME_OPTION_MIN)
{
g_game_vars.music_volume--;
}
else if (key_hit(KEY_RIGHT) && g_game_vars.music_volume < VOLUME_OPTION_MAX)
{
g_game_vars.music_volume++;
}
update_volume_slider_graphics(MUSIC_VOLUME_BTN_IDX);
mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX);
return true;
}
static bool sound_volume_row_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
change_button_highlight(row_idx, prev_selection, new_selection);
if (prev_selection->y != new_selection->y)
return true;
if (key_hit(KEY_LEFT) && g_game_vars.sound_volume > VOLUME_OPTION_MIN)
{
g_game_vars.sound_volume--;
}
else if (key_hit(KEY_RIGHT) && g_game_vars.sound_volume < VOLUME_OPTION_MAX)
{
g_game_vars.sound_volume++;
}
update_volume_slider_graphics(SOUND_VOLUME_BTN_IDX);
return true;
}
+4 -3
View File
@@ -1,6 +1,7 @@
#include "joker.h"
#include "card.h"
#include "game_variables.h"
#include "graphic_utils.h"
#include "joker_gfx.h"
#include "pool.h"
@@ -271,7 +272,7 @@ bool joker_object_score(
u32 chips = get_chips();
u32 mult = get_mult();
int money = get_money();
int money = g_game_vars.money;
if (effect_flags_ret & JOKER_EFFECT_FLAG_RETRIGGER)
{
@@ -345,7 +346,7 @@ bool joker_object_score(
// Update values
set_chips(chips);
set_mult(mult);
set_money(money);
g_game_vars.money = money;
// Update displays
display_chips();
@@ -367,7 +368,7 @@ Sprite* joker_object_get_sprite(JokerObject* joker_object)
int joker_get_random_rarity()
{
int joker_rarity = 0;
int rarity_roll = random() % 100;
int rarity_roll = get_rand() % 100;
if (rarity_roll < COMMON_JOKER_CHANCE)
{
joker_rarity = COMMON_JOKER;
+7 -7
View File
@@ -562,7 +562,7 @@ static u32 misprint_joker_effect(
*joker_effect = &shared_joker_effect;
(*joker_effect)->mult = random() % (MISPRINT_MAX_MULT + 1);
(*joker_effect)->mult = get_rand() % (MISPRINT_MAX_MULT + 1);
return JOKER_EFFECT_FLAG_MULT;
}
@@ -775,7 +775,7 @@ static u32 reserved_parking_joker_effect(
u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE;
if ((random() % 2 == 0) && card_is_face(scored_card))
if ((get_rand() % 2 == 0) && card_is_face(scored_card))
{
*joker_effect = &shared_joker_effect;
@@ -797,7 +797,7 @@ static u32 business_card_joker_effect(
u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE;
if ((random() % 2 == 0) && card_is_face(scored_card))
if ((get_rand() % 2 == 0) && card_is_face(scored_card))
{
*joker_effect = &shared_joker_effect;
@@ -885,11 +885,11 @@ static u32 bull_joker_effect(
// The wiki says it does nothing if money is 0 or below
// This allows us to avoid scoring negative Chips
if (get_money() > 0)
if (g_game_vars.money > 0)
{
*joker_effect = &shared_joker_effect;
(*joker_effect)->chips = get_money() * 2;
(*joker_effect)->chips = g_game_vars.money * 2;
effect_flags_ret = JOKER_EFFECT_FLAG_CHIPS;
}
@@ -1154,11 +1154,11 @@ static u32 bootstraps_joker_effect(
u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE;
// Same protection as the Bull Joker
if (get_money() > 0)
if (g_game_vars.money > 0)
{
*joker_effect = &shared_joker_effect;
(*joker_effect)->mult = (get_money() / 5) * 2;
(*joker_effect)->mult = (g_game_vars.money / 5) * 2;
effect_flags_ret = JOKER_EFFECT_FLAG_MULT;
}
+166
View File
@@ -0,0 +1,166 @@
/**
* @file save.c
*/
#include "save.h"
#include "audio_utils.h"
#include "game.h"
#include "joker.h"
#include "list.h"
#include "util.h"
#include "version.h"
#include <stdlib.h>
#include <string.h>
// See https://gbadev.net/gbadoc/memory.html for more details on SRAM
// A few important pieces of info:
// - Read/Writes are limited to 8-bits words so they are done byte per byte
// - Memory is filled with 1s by default
// (at least in mgba, not sure about real HW)
#define CHECK_BASE 0x0000
#define GAME_BASE 0x0010
#define LISTS_BASE 0x0060
#define CHECK_MAGIC 0x4C414247 // Spells GBAL, used to determine if the save data is junk
#define CHECK_HASH_SIZE 7
#define GIT_HASH_START 17 // starts after "GBALATRO-VERSION:" in the gbalatro_version var
// clang-format off
/**
* @brief SaveHeader for validation checks
*
* Structure holding save data header info to be packed and written to SRAM for validation.
* Defined in this discussion as follows: https://github.com/GBALATRO/balatro-gba/discussions/450
* word | Byte 0 | Byte 1 | Byte 2 | Byte 3 | name | purpose
* -----|--------|--------|--------|--------|--------------|------------------------------------------------------------------
* 0 | 0x47 | 0x42 | 0x41 | 0x4C | MAGIC | Identify if proceeding data is valid and not junk, spells "GBAL"
* 1 | Dirty | H[0] | H[1] | H[2] | GITHASH_LOW | Dirty flag, followed by the first 3 bytes of shortened git hash H
* 2 | H[3] | H[4] | H[5] | H[6] | GITHASH_HIGH | Last 4 bytes of shortened git hash H, with a dirty flag
*/
// clang-format on
typedef struct SaveHeader
{
u32 magic;
bool dirty;
char githash[CHECK_HASH_SIZE];
} SaveHeader;
/**
* @brief Write raw binary data to SRAM
*
* @param sram_base address written to in the SRAM
* @param bytes pointer to the written data
* @param size number of bytes written
*/
static inline void write_sram(u32 sram_base, const u8* bytes, u32 size)
{
if (sram_base + size > SRAM_SIZE)
return;
for (u32 i = 0; i < size; i++)
{
sram_mem[sram_base + i] = bytes[i];
}
}
/**
* @brief Read raw binary data from SRAM
*
* @sa write_sram
*/
static inline void read_sram(u32 sram_base, u8* bytes, u32 size)
{
if (sram_base + size > SRAM_SIZE)
return;
for (u32 i = 0; i < size; i++)
{
bytes[i] = sram_mem[sram_base + i];
}
}
/**
* @brief Checks the 7 chars of gbalatro_version after the "GBALATRO_VERSION" prefix
* representing the git hash of the code the build is based on.
*
* @returns true if the git hash of the ROM is equal to the hash saved in SRAM.
* false if they are different.
*/
static inline bool check_hash(const char* prefix)
{
for (u32 i = 0; i < CHECK_HASH_SIZE; i++)
{
if (gbalatro_version[GIT_HASH_START + i] != prefix[i])
{
return false;
}
}
return true;
}
/**
* @brief Determines if the current build is considered "dirty" aka has uncommitted changes.
* This works because the gbalatro_version string has "-dirty" added at the end if it's
* dirty.
*
* @returns true if version is dirty, false otherwise.
*/
static inline bool is_version_dirty()
{
return strlen(gbalatro_version) > GIT_HASH_START + CHECK_HASH_SIZE;
}
/**
* @brief Writes a magic number and ROM version info to SRAM to signal that the
* save data exists and allow the game to determine if it is compatible.
*/
static inline void set_save_header()
{
SaveHeader check = {};
check.magic = CHECK_MAGIC;
check.dirty = is_version_dirty();
memcpy(&(check.githash), gbalatro_version + GIT_HASH_START, CHECK_HASH_SIZE);
write_sram(CHECK_BASE, (const u8*)&check, sizeof(check));
}
/**
* @brief Reads whether the save data exists and is valid.
*
* @sa set_save_valid
*/
static inline bool check_save_header()
{
SaveHeader check;
read_sram(CHECK_BASE, (u8*)&check, sizeof(check));
bool is_valid = (check.magic == CHECK_MAGIC) && check.dirty == is_version_dirty() &&
check_hash(check.githash);
return is_valid;
}
void save_game(void)
{
set_save_header();
write_sram(GAME_BASE, (const u8*)&g_game_vars, sizeof(g_game_vars));
}
void load_game(void)
{
if (!check_save_header())
return;
read_sram(GAME_BASE, (u8*)&g_game_vars, sizeof(g_game_vars));
// return to where we were in the random sequence so that the run stays reproducible
for (u32 i = 0; i < g_game_vars.rng_step; i++)
{
(void)rand();
}
mmSetModuleVolume(MM_MODULE_FULL_VOLUME * g_game_vars.music_volume / VOLUME_OPTION_MAX);
}
+4 -1
View File
@@ -125,6 +125,9 @@ void selection_grid_process_input(SelectionGrid* selection_grid)
{
// To make the next line shorter and more readable
Selection* selection = &selection_grid->selection;
selection_grid->rows[selection->y].on_key_transit(selection_grid, selection);
if (selection_grid->rows[selection->y].on_key_transit != NULL)
{
selection_grid->rows[selection->y].on_key_transit(selection_grid, selection);
}
}
}
+3 -3
View File
@@ -11,7 +11,7 @@
static const Rect COUNTDOWN_TIMER_RECT = {208, 144, 240, 152};
static uint timer = 0;
void splash_screen_on_init()
void splash_screen_on_init(void)
{
timer = 0;
@@ -25,7 +25,7 @@ void splash_screen_on_init()
tte_printf("#{P:8,144; cx:0xF000}(Press any key to skip)");
}
void splash_screen_on_update()
void splash_screen_on_update(void)
{
timer++;
@@ -49,7 +49,7 @@ void splash_screen_on_update()
tte_erase_screen();
}
void splash_screen_on_exit()
void splash_screen_on_exit(void)
{
mmStart(MOD_MAIN_THEME, MM_PLAY_LOOP);
}
+4 -3
View File
@@ -2,6 +2,7 @@
#include "audio_utils.h"
#include "game.h"
#include "game_variables.h"
#include "pool.h"
#include "soundbank.h"
#include "util.h"
@@ -209,8 +210,8 @@ void sprite_object_reset_transform(SpriteObject* sprite_object)
void sprite_object_update(SpriteObject* sprite_object)
{
sprite_object->vx += ((sprite_object->tx - sprite_object->x) * get_game_speed()) / 8;
sprite_object->vy += ((sprite_object->ty - sprite_object->y) * get_game_speed()) / 8;
sprite_object->vx += ((sprite_object->tx - sprite_object->x) * g_game_vars.game_speed) / 8;
sprite_object->vy += ((sprite_object->ty - sprite_object->y) * g_game_vars.game_speed) / 8;
// Scale up the card when it's played
sprite_object->vscale += (sprite_object->tscale - sprite_object->scale) / 8;
@@ -310,7 +311,7 @@ void sprite_object_set_focus(SpriteObject* sprite_object, bool focus)
play_sfx(
SFX_CARD_FOCUS,
MM_BASE_PITCH_RATE + rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE,
SFX_DEFAULT_VOLUME
);
sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX);
+2 -2
View File
@@ -6,5 +6,5 @@
#define GIT_DIRTY "-dirty"
#endif
__attribute__((section(".version"), used)) const char balatro_version[] =
"GBALATRO_VERSION:" GIT_HASH GIT_DIRTY;
__attribute__((section(".version"), used)) const char gbalatro_version[] =
"GBALATRO-VERSION:" GIT_HASH GIT_DIRTY;