1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-20 08:54:15 +00:00

add layoff support for menu

This commit is contained in:
jugeeya 2019-10-26 14:09:18 -07:00
parent 95a7e98332
commit e244f9d29c
13 changed files with 61 additions and 226 deletions

View file

@ -47,7 +47,7 @@ CFLAGS := -Wall -O2 \
CFLAGS += $(INCLUDE) -DSWITCH
CXXFLAGS := $(CFLAGS) -fno-rtti -Wno-parentheses -Wno-write-strings -Wno-int-to-pointer-cast -std=gnu++11
CXXFLAGS := $(CFLAGS) -g3 -fno-rtti -Wno-parentheses -Wno-write-strings -Wno-int-to-pointer-cast -std=gnu++11
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(TOPDIR)/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)

@ -1 +1 @@
Subproject commit 7b435a8147524d6a87fc2018126b30f798a6969a
Subproject commit a31fc22e1859da6f217b61cdd17afa54f5a105fb

View file

@ -10,7 +10,7 @@
#include "acmd_wrapper.h"
#include "lib/l2c_imports.h"
#include "saltysd/saltysd_helper.h"
#include "taunt_toggles.h"
#include "training/common.hpp"
#include "useful/const_value_table.h"
#include "useful/raygun_printer.h"
@ -176,8 +176,7 @@ void ATTACK_replace(u64 a1) {
l2c_agent.get_lua_stack(15, &z2); // float or void
// hacky way of forcing no shield damage on all hitboxes
if (is_training_mode() &&
TOGGLE_STATE == SHIELD_TOGGLES && SHIELD_STATE == SHIELD_INFINITE) {
if (is_training_mode() && menu.SHIELD_STATE == SHIELD_INFINITE) {
L2CValue hitbox_params[36];
for (size_t i = 0; i < 36; i++)
l2c_agent.get_lua_stack(i + 1, &hitbox_params[i]);
@ -196,7 +195,7 @@ void ATTACK_replace(u64 a1) {
// original code: parse lua stack and call AttackModule::set_attack()
AttackModule_set_attack_lua_state(LOAD64(LOAD64(a1 - 8) + 416LL), a1);
if (HITBOX_VIS && is_training_mode()) { // generate hitbox effect(s)
if (menu.HITBOX_VIS && is_training_mode()) { // generate hitbox effect(s)
float color_scale;
if (false) { // color intensity scales with damage
color_scale = unlerp_bounded(1.0f, 18.0f, damage.raw_float);
@ -267,7 +266,7 @@ void CATCH_replace(u64 a1) {
// jump to Catch_jumpback
asm("BLR X9");
if (HITBOX_VIS && is_training_mode()) {
if (menu.HITBOX_VIS && is_training_mode()) {
Vector3f color = ID_COLORS[(id.raw + 3) % 8];
generate_hitbox_effects(&l2c_agent, &joint, &size, &x, &y, &z, &x2, &y2, &z2, &color);
}

View file

@ -101,6 +101,14 @@ int main(int argc, char* argv[]) {
// Add function replacements here
hitbox_vis_main();
training_mods_main();
FILE* f = SaltySDCore_fopen("sdmc:/SaltySD/training_modpack.log", "w");
if (f) {
SaltySD_printf("Writing training_modpack.log...\n");
char buffer[20];
snprintf(buffer, 20, "%lx", (u64)&menu);
SaltySDCore_fwrite(buffer, strlen(buffer), 1, f);
SaltySDCore_fclose(f);
}
__libnx_exit(0);
}

View file

@ -1,19 +1,11 @@
#ifndef TAUNT_TOGGLES_H
#define TAUNT_TOGGLES_H
u64 is_training_mode(void) asm("_ZN3app9smashball16is_training_modeEv") LINKABLE;
#define NONE 0
// Up Taunt
bool HITBOX_VIS = 1;
// Side Taunt
// DI
float DI_stick_x = 0;
float DI_stick_y = 0;
int DI_STATE = NONE;
#define SET_DI 1
#define DI_RANDOM_IN_AWAY 2
#define NUM_DI_STATES 3
@ -28,9 +20,7 @@ int DI_STATE = NONE;
#define MASH_SIDE_B 6
#define MASH_UP_B 7
#define MASH_DOWN_B 8
int ATTACK_STATE = MASH_NAIR;
#define NUM_ATTACK_STATES 9
const char* attack_items[] = { "Neutral Air", "Forward Air", "Back Air", "Up Air", "Down Air", "Neutral B", "Side B", "Up B", "Down B" };
// Ledge Option
#define RANDOM_LEDGE 1
@ -38,44 +28,39 @@ int ATTACK_STATE = MASH_NAIR;
#define ROLL_LEDGE 3
#define JUMP_LEDGE 4
#define ATTACK_LEDGE 5
int LEDGE_STATE = RANDOM_LEDGE;
#define NUM_LEDGE_STATES 6
const char* ledge_items[] = { "Random", "Neutral Getup", "Roll", "Jump", "Attack" };
// Tech Option
#define RANDOM_TECH 1
#define TECH_IN_PLACE 2
#define TECH_ROLL 3
#define TECH_MISS 4
int TECH_STATE = RANDOM_TECH;
#define NUM_TECH_STATES 5
// Down Taunt
#define MASH_TOGGLES 0
#define ESCAPE_TOGGLES 1
#define SHIELD_TOGGLES 2
int TOGGLE_STATE = MASH_TOGGLES;
#define NUM_TOGGLE_STATES 3
const char* tech_items[] = { "Random", "In-Place", "Roll", "Miss Tech" };
// Mash States
#define MASH_AIRDODGE 1
#define MASH_JUMP 2
#define MASH_ATTACK 3
#define MASH_RANDOM 4
int MASH_STATE = NONE;
#define NUM_MASH_STATES 5
// Escape States
#define ESCAPE_LEDGE 1
#define ESCAPE_TECH 2
int ESCAPE_STATE = ESCAPE_LEDGE;
#define NUM_ESCAPE_STATES 3
const char* mash_items[] = { "None", "Airdodge", "Jump", "Attack", "Random" };
// Shield States
#define SHIELD_INFINITE 1
#define SHIELD_HOLD 2
int SHIELD_STATE = NONE;
#define NUM_SHIELD_STATES 3
const char* shield_items[] = { "None", "Infinite", "Hold" };
struct TrainingModpackMenu {
bool HITBOX_VIS = 1;
float DI_stick_x = 0;
float DI_stick_y = 0;
int DI_STATE = NONE;
int ATTACK_STATE = MASH_NAIR;
int LEDGE_STATE = RANDOM_LEDGE;
int TECH_STATE = RANDOM_TECH;
int MASH_STATE = NONE;
int SHIELD_STATE = NONE;
char print_buffer[256];
u64 print_buffer_len = 0;
} menu;
#endif // TAUNT_TOGGLES_H

View file

@ -3,7 +3,10 @@
#include "useful/const_value_table.h"
#include "../taunt_toggles.h"
using namespace app::lua_bind;
u64 fighter_manager_addr;
u64 is_training_mode(void) asm("_ZN3app9smashball16is_training_modeEv") LINKABLE;
u8 get_category(u64 module_accessor) {
return (u8)(*(u32*)(module_accessor + 8) >> 28);

View file

@ -6,12 +6,12 @@ float get_float(u64 module_accessor, int var, bool& replace) {
var == FIGHTER_STATUS_DAMAGE_WORK_FLOAT_VECOR_CORRECT_STICK_Y) {
if (is_training_mode() && is_operation_cpu(module_accessor) &&
is_in_hitstun(module_accessor)) {
if (DI_STATE != NONE) {
if (menu.DI_STATE != NONE) {
float stick_x = 0.0, stick_y = 0.0;
if (DI_STATE == SET_DI) {
stick_x = DI_stick_x;
stick_y = DI_stick_y;
} else if (DI_STATE == DI_RANDOM_IN_AWAY) {
if (menu.DI_STATE == SET_DI) {
stick_x = menu.DI_stick_x;
stick_y = menu.DI_stick_y;
} else if (menu.DI_STATE == DI_RANDOM_IN_AWAY) {
// either 1.0 or -1.0
stick_x = (float)(app::sv_math::rand(hash40("fighter"), 2) * 2.0) - 1;
stick_y = 0.0;

View file

@ -13,9 +13,9 @@ void force_option(u64 module_accessor) {
if (frame == random_frame || frame > 30.0) {
int status = 0;
int ledge_case = LEDGE_STATE - 1;
int ledge_case = menu.LEDGE_STATE - 1;
if (LEDGE_STATE == RANDOM_LEDGE)
if (menu.LEDGE_STATE == RANDOM_LEDGE)
ledge_case = app::sv_math::rand(hash40("fighter"), 4) + 1;
switch (ledge_case) {
@ -60,7 +60,7 @@ void defensive_option(u64 module_accessor, int category, int& flag) {
}
void get_command_flag_cat(u64 module_accessor, int category, int& flag) {
if (LEDGE_STATE != NONE && is_training_mode() && is_operation_cpu(module_accessor)) {
if (menu.LEDGE_STATE != NONE && is_training_mode() && is_operation_cpu(module_accessor)) {
force_option(module_accessor);
defensive_option(module_accessor, category, flag);
}

View file

@ -4,9 +4,9 @@ namespace Mash {
int get_attack_air_kind(u64 module_accessor, bool& replace) {
int kind = 0;
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (MASH_STATE == MASH_ATTACK) {
if (menu.MASH_STATE == MASH_ATTACK) {
replace = true;
switch (ATTACK_STATE) {
switch (menu.ATTACK_STATE) {
case MASH_NAIR:
kind = FIGHTER_COMMAND_ATTACK_AIR_KIND_N; break;
case MASH_FAIR:
@ -21,7 +21,7 @@ int get_attack_air_kind(u64 module_accessor, bool& replace) {
return kind;
}
if (MASH_STATE == MASH_RANDOM) {
if (menu.MASH_STATE == MASH_RANDOM) {
replace = true;
return app::sv_math::rand(hash40("fighter"), 5) + 1;
}
@ -35,17 +35,17 @@ void get_command_flag_cat(u64 module_accessor, int category, int& flag) {
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (is_in_hitstun(module_accessor) || is_in_landing(module_accessor)) {
if (MASH_STATE == MASH_AIRDODGE)
if (menu.MASH_STATE == MASH_AIRDODGE)
if (category == FIGHTER_PAD_COMMAND_CATEGORY1)
flag |= FIGHTER_PAD_CMD_CAT1_FLAG_AIR_ESCAPE;
if (MASH_STATE == MASH_JUMP)
if (menu.MASH_STATE == MASH_JUMP)
if (category == FIGHTER_PAD_COMMAND_CATEGORY1)
flag |= FIGHTER_PAD_CMD_CAT1_FLAG_JUMP_BUTTON;
if (MASH_STATE == MASH_ATTACK)
if (menu.MASH_STATE == MASH_ATTACK)
if (category == FIGHTER_PAD_COMMAND_CATEGORY1) {
switch (ATTACK_STATE) {
switch (menu.ATTACK_STATE) {
case MASH_NAIR:
case MASH_FAIR:
case MASH_BAIR:
@ -68,7 +68,7 @@ void get_command_flag_cat(u64 module_accessor, int category, int& flag) {
}
}
if (MASH_STATE == MASH_RANDOM)
if (menu.MASH_STATE == MASH_RANDOM)
if (category == FIGHTER_PAD_COMMAND_CATEGORY1) {
int situation_kind =
StatusModule::situation_kind(module_accessor);
@ -128,7 +128,7 @@ void get_command_flag_cat(u64 module_accessor, int category, int& flag) {
bool check_button_on(u64 module_accessor, int button, bool& replace) {
if (button == CONTROL_PAD_BUTTON_GUARD_HOLD || button == CONTROL_PAD_BUTTON_GUARD) {
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (MASH_STATE == MASH_AIRDODGE && (is_in_hitstun(module_accessor) || is_in_landing(module_accessor))) {
if (menu.MASH_STATE == MASH_AIRDODGE && (is_in_hitstun(module_accessor) || is_in_landing(module_accessor))) {
replace = true;
return true;
}

View file

@ -1,143 +0,0 @@
#include "common.hpp"
extern int vsnprintf(char* s, size_t maxlen, const char* format, va_list arg) LINKABLE;
int vsnprintf_intercept(char* s, size_t maxlen, const char* format, va_list arg) {
if (strcmp(format, "mel_training_help_shift0") == 0) {
TOGGLE_STATE = MASH_TOGGLES;
switch (MASH_STATE) {
case NONE:
format = "mel_shortmsg_1"; break;
case MASH_AIRDODGE:
format = "mel_shortmsg_2"; break;
case MASH_JUMP:
format = "mel_shortmsg_3"; break;
case MASH_RANDOM:
format = "mel_shortmsg_4"; break;
case MASH_ATTACK:
format = "mel_shortmsg_5"; break;
}
} else if (strcmp(format, "mel_training_help_shift1") == 0) {
TOGGLE_STATE = ESCAPE_TOGGLES;
switch (ESCAPE_STATE) {
case NONE:
format = "mel_shortmsg_6"; break;
case ESCAPE_LEDGE:
format = "mel_shortmsg_7"; break;
case ESCAPE_TECH:
format = "mel_shortmsg_8"; break;
}
} else if (strcmp(format, "mel_training_help_shift2") == 0) {
TOGGLE_STATE = SHIELD_TOGGLES;
switch (SHIELD_STATE) {
case NONE:
format = "mel_shortmsg_9"; break;
case SHIELD_INFINITE:
format = "mel_shortmsg_10"; break;
case SHIELD_HOLD:
format = "mel_shortmsg_11"; break;
}
}
// For Shulk
if (strcmp(format, "mel_info_fighter_shulk_special_00") == 0)
format = "mel_shortmsg_101"; // SMASH
else if (strcmp(format, "mel_info_fighter_shulk_special_03") == 0)
format = "mel_shortmsg_102"; // SPEED
else if (strcmp(format, "mel_info_fighter_shulk_special_02") == 0)
format = "mel_shortmsg_103"; // SHIELD
if (strcmp(format, "mel_training_shift0") == 0)
format = "mel_info_fighter_shulk_special_00"; // SMASH
else if (strcmp(format, "mel_training_shift1") == 0)
format = "mel_info_fighter_shulk_special_03"; // SPEED
else if (strcmp(format, "mel_training_shift2") == 0)
format = "mel_info_fighter_shulk_special_02"; // SHIELD
return vsnprintf(s, maxlen, format, arg);
}
namespace Selection {
void menu_replace() {
SaltySDCore_ReplaceImport("vsnprintf", (void*)vsnprintf_intercept);
}
void clear_command(u64 module_accessor, u64 motion_kind) {
if (motion_kind == hash40("appeal_lw_l") || motion_kind == hash40("appeal_lw_r")) {
if (is_training_mode()) {
if (TOGGLE_STATE == MASH_TOGGLES) {
MASH_STATE = (MASH_STATE + 1) % NUM_MASH_STATES;
const char* toggle_strings[NUM_MASH_STATES] =
{"NONE", "AIRDODGE", "JUMP", "ATTACK", "RANDOM"};
print_string(module_accessor, toggle_strings[MASH_STATE]);
}
if (TOGGLE_STATE == ESCAPE_TOGGLES) {
ESCAPE_STATE = (ESCAPE_STATE + 1) % NUM_ESCAPE_STATES;
const char* toggle_strings[NUM_ESCAPE_STATES] =
{"NONE", "LEDGE", "TECH"};
print_string(module_accessor, toggle_strings[ESCAPE_STATE]);
}
if (TOGGLE_STATE == SHIELD_TOGGLES) {
SHIELD_STATE = (SHIELD_STATE + 1) % NUM_SHIELD_STATES;
const char* toggle_strings[NUM_SHIELD_STATES] =
{"NONE", "INFINITE", "HOLD"};
print_string(module_accessor, toggle_strings[SHIELD_STATE]);
}
}
} else if (motion_kind == hash40("appeal_s_l") || motion_kind == hash40("appeal_s_r")) {
if (is_training_mode()) {
if (TOGGLE_STATE == ESCAPE_TOGGLES &&
ESCAPE_STATE == ESCAPE_LEDGE) {
LEDGE_STATE = (LEDGE_STATE + 1) % NUM_LEDGE_STATES;
const char* LEDGE_strings[NUM_LEDGE_STATES] =
{"NONE", "RANDOM", "NORMAL", "ROLL", "JUMP", "ATTACK"};
print_string(module_accessor, LEDGE_strings[LEDGE_STATE]);
} else if (TOGGLE_STATE == ESCAPE_TOGGLES &&
ESCAPE_STATE == ESCAPE_TECH) {
TECH_STATE = (TECH_STATE + 1) % NUM_TECH_STATES;
const char* TECH_strings[NUM_TECH_STATES] =
{"NONE", "RANDOM", "IN PLACE", "ROLL", "MISS TECH"};
print_string(module_accessor, TECH_strings[TECH_STATE]);
} else if (MASH_STATE == MASH_ATTACK) {
ATTACK_STATE = (ATTACK_STATE + 1) % NUM_ATTACK_STATES;
const char* ATTACK_strings[NUM_ATTACK_STATES] =
{"NAIR", "FAIR", "BAIR", "UPAIR", "DAIR",
"NEUTRAL B", "SIDE B", "UP B", "DOWN B"};
print_string(module_accessor,
ATTACK_strings[ATTACK_STATE]);
} else {
if (ControlModule::check_button_on(module_accessor, CONTROL_PAD_BUTTON_APPEAL_S_L)) {
DI_STATE = DI_STATE == NONE ? DI_RANDOM_IN_AWAY : NONE;
} else {
DI_STATE = DI_STATE == NONE ? SET_DI : NONE;
}
const char* DI_strings[NUM_DI_STATES] =
{"NONE", "SET_DI", "RANDOM\nIN AWAY"};
print_string(module_accessor, DI_strings[DI_STATE]);
if (DI_STATE == SET_DI) {
DI_stick_x = ControlModule::get_stick_x(module_accessor);
DI_stick_y = ControlModule::get_stick_y(module_accessor);
}
}
}
} else if (motion_kind == hash40("appeal_hi_l") || motion_kind == hash40("appeal_hi_r")) {
if (is_training_mode()) {
HITBOX_VIS = !HITBOX_VIS;
if (HITBOX_VIS)
print_string(module_accessor, "HITBOX\nVIS");
else
print_string(module_accessor, "NO\nHITBOX");
}
}
}
}

View file

@ -4,7 +4,7 @@
namespace Shield {
float get_param_float(u64 module_accessor, u64 param_type, u64 param_hash, bool& replace) {
if (is_training_mode()) {
if (SHIELD_STATE == SHIELD_INFINITE) {
if (menu.SHIELD_STATE == SHIELD_INFINITE) {
if (param_type == hash40("common")) {
if (param_hash == hash40("shield_dec1")) {
replace = true;
@ -30,7 +30,7 @@ float get_param_float(u64 module_accessor, u64 param_type, u64 param_hash, bool&
bool check_button_on(u64 module_accessor, int button, bool& replace) {
if (button == CONTROL_PAD_BUTTON_GUARD_HOLD || button == CONTROL_PAD_BUTTON_GUARD) {
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (SHIELD_STATE == SHIELD_HOLD || SHIELD_STATE == SHIELD_INFINITE) {
if (menu.SHIELD_STATE == SHIELD_HOLD || menu.SHIELD_STATE == SHIELD_INFINITE) {
replace = true;
return true;
}
@ -44,7 +44,7 @@ bool check_button_on(u64 module_accessor, int button, bool& replace) {
bool check_button_off(u64 module_accessor, int button, bool& replace) {
if (button == CONTROL_PAD_BUTTON_GUARD_HOLD || button == CONTROL_PAD_BUTTON_GUARD) {
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (SHIELD_STATE == SHIELD_HOLD || SHIELD_STATE == SHIELD_INFINITE) {
if (menu.SHIELD_STATE == SHIELD_HOLD || menu.SHIELD_STATE == SHIELD_INFINITE) {
replace = true;
return false;
}

View file

@ -4,7 +4,7 @@ namespace Tech {
void init_settings(u64 module_accessor, int status_kind, bool& replace) {
if (is_training_mode() && is_operation_cpu(module_accessor)) {
if (status_kind == FIGHTER_STATUS_KIND_DOWN) {
if (TECH_STATE == RANDOM_TECH) {
if (menu.TECH_STATE == RANDOM_TECH) {
const int NUM_TECH_STATUSES = 3;
int random_statuses[NUM_TECH_STATUSES] = {
FIGHTER_STATUS_KIND_DOWN,
@ -18,11 +18,11 @@ void init_settings(u64 module_accessor, int status_kind, bool& replace) {
replace = true;
return;
}
} else if (TECH_STATE == TECH_IN_PLACE) {
} else if (menu.TECH_STATE == TECH_IN_PLACE) {
StatusModule::change_status_request_from_script(module_accessor, FIGHTER_STATUS_KIND_PASSIVE, 1);
replace = true;
return;
} else if (TECH_STATE == TECH_ROLL) {
} else if (menu.TECH_STATE == TECH_ROLL) {
StatusModule::change_status_request_from_script(module_accessor, FIGHTER_STATUS_KIND_PASSIVE_FB, 1);
replace = true;
return;
@ -47,7 +47,7 @@ void init_settings(u64 module_accessor, int status_kind, bool& replace) {
}
void get_command_flag_cat(u64 module_accessor, int category, int& flag) {
if (TECH_STATE != NONE && is_training_mode() && is_operation_cpu(module_accessor)) {
if (menu.TECH_STATE != NONE && is_training_mode() && is_operation_cpu(module_accessor)) {
int prev_status = StatusModule::prev_status_kind(module_accessor, 0);
int status = StatusModule::status_kind(module_accessor);
if (status == FIGHTER_STATUS_KIND_DOWN_WAIT || status == FIGHTER_STATUS_KIND_DOWN_WAIT_CONTINUE) {

View file

@ -19,7 +19,6 @@
#include "training/directional_influence.hpp"
#include "training/ledge.hpp"
#include "training/mash.hpp"
#include "training/selection.hpp"
#include "training/shield.hpp"
#include "training/tech.h"
#include "training/input_recorder.hpp"
@ -77,7 +76,7 @@ int get_command_flag_cat_replace(u64 module_accessor, int category) {
int status_kind = StatusModule::status_kind(module_accessor);
MotionAnimcmdModule::set_sleep_effect(module_accessor,
is_training_mode() &&
HITBOX_VIS &&
menu.HITBOX_VIS &&
!(status_kind >= FIGHTER_STATUS_KIND_CATCH && status_kind <= FIGHTER_STATUS_KIND_TREAD_FALL));
u64 control_module = load_module(module_accessor, 0x48);
@ -152,16 +151,6 @@ bool check_button_off_replace(u64 module_accessor, int button) {
bool (*check_button_off)(u64, int) = (bool (*)(u64, int)) load_module_impl(control_module, 0x268);
return check_button_off(control_module, button);
}
void clear_command_replace(u64 module_accessor, bool unk1) {
Selection::clear_command(module_accessor, MotionModule::motion_kind(module_accessor));
u64 control_module = load_module(module_accessor, 0x48);
void (*clear_command)(u64, bool) =
(void (*)(u64, bool)) load_module_impl(control_module, 0x358);
clear_command(control_module, unk1);
}
} // namespace ControlModule
namespace StatusModule {
@ -182,10 +171,6 @@ void init_settings_replace(u64 module_accessor, int situationKind, int unk1, uin
void training_mods_main() {
fighter_manager_addr = SaltySDCore_FindSymbol(
"_ZN3lib9SingletonIN3app14FighterManagerEE9instance_E");
// Taunt toggles
SaltySD_function_replace_sym(
"_ZN3app8lua_bind33ControlModule__clear_command_implEPNS_26BattleObjectModuleAccessorEb",
(u64)&ControlModule::clear_command_replace);
// Mash airdodge/jump
SaltySD_function_replace_sym_check_prev(
@ -226,8 +211,6 @@ void training_mods_main() {
SaltySD_function_replace_sym(
"_ZN3app8lua_bind32StatusModule__init_settings_implEPNS_26BattleObjectModuleAccessorENS_13SituationKindEijNS_20GroundCliffCheckKindEbiiii",
(u64)&StatusModule::init_settings_replace);
Selection::menu_replace();
}
#endif // TRAINING_MODS_H