Skip to content

Commit 218a72c

Browse files
committed
MC_CAN_SELECT_CHARACTER (allows preventing starting a run with a character from character selection, including via random)
Added an optional boolean parameter to Isaac.StartNewGame to set Seeds.IsCustomRun Optionally allow passing a whole Seeds object to Isaac.StartNewGame instead of just the start seed int
1 parent 8ff698d commit 218a72c

File tree

13 files changed

+122
-38
lines changed

13 files changed

+122
-38
lines changed

changelog.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ Added:
2929
Called before Game():BombTearflagEffects(), used when TearFlags-based effects are triggered by an explosion.
3030
Return false to cancel, or a table containing changed values for Position, Radius, TearFlags, and/or RadiusMult.
3131
Optional param is the EntityType of the Source entity, if one exists (the Source can be nil).
32+
- MC_CAN_SELECT_CHARACTER(PlayerType, bool IsBeingSelected)
33+
Will only run for characters present in the character select menu that are capable of being selected (IE, not hidden or locked behind an achievement).
34+
Return false to prevent the character from being selectable. This has no visual effects.
35+
IsBeingSelected is true if the player is actually trying to start a run with the character. Returning false in this case will play the error buzzer sound.
36+
Otherwise, this is just a check by some other logic (such as deciding which characters are eligible for random character selection).
37+
Optional param: PlayerType
3238
* Added EntitySaveStateManager class to support persistent, modded save data for entities.
3339
* EntityLaser:
3440
- SetInitSound(SoundEffect sound)
@@ -60,6 +66,10 @@ Added:
6066
* The "Error" and "Modeling Clay" trinkets now use the sprite of the item they are mimicking when smelted, like how they do in later versions of Repentance+.
6167
/newline/
6268
Modified:
69+
* Isaac:
70+
- StartNewGame
71+
Added optional param "bool isCustomRun".
72+
Added function variant that takes a whole Seeds object instead of a seed integer.
6373
* EntityPlayer:
6474
- SetControllerIndex(integer ControllerIndex, boolean IncludePlayerOwned = false)
6575
If IncludePlayerOwned set to true, then sets ControllerIndex for player's subplayer/twinplayer

libzhl/IsaacRepentance_static.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,27 @@ int HistoryHUD::GetNumVisibleItems() const {
607607
return GetNumRows() * GetNumColumns();
608608
}
609609

610+
int Menu_Character::GetPlayerTypeFromMenuID(int menuID, bool taintedMenu) {
611+
if (menuID > 0) {
612+
if (menuID < 18) {
613+
return __ptr_g_MenuCharacterEntries[taintedMenu ? (menuID + 18) : menuID].playerType;
614+
} else if (menuID < (int)g_ModCharacterMap.size() + 18) {
615+
const auto& modChar = g_ModCharacterMap[menuID - 18];
616+
return taintedMenu ? modChar.tainted : modChar.normal;
617+
}
618+
}
619+
return -1;
620+
}
621+
622+
int Menu_Character::GetPlayerTypeFromCurrentMenuID(int menuID) {
623+
const bool taintedMenu = GetSelectedCharacterMenu() == 1;
624+
return Menu_Character::GetPlayerTypeFromMenuID(menuID, taintedMenu);
625+
}
626+
627+
int Menu_Character::GetSelectedPlayerType() {
628+
return GetPlayerTypeFromCurrentMenuID(SelectedCharacterID);
629+
}
630+
610631
bool Isaac::IsInGame() {
611632
return g_Manager->GetState() == 2 && g_Game;
612633
}

libzhl/functions/ASM.zhl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ asm DisableExitPrompt "8d45??c745??00009643"; //DisableExitPrompt option patch
55
asm LeaderboardGoalPatch "83be????????01b807000000"; //LeaderboardGoalPatch
66

77
asm HideModdedCharacterByAchievement "80b8????????0075??83b8????????0075??8b08"; //PatchModdedCharacterHiddenByAchievementInMenu
8+
asm MenuCharacter_PreSelectCharacter "74??a1????????83ec14";
89

910
asm RenderCharacterWheel_RenderPositionStackOffset "f30f114c24??f30f58d0f30f115424"; // Offset is one byte @ +0x5
1011
asm RenderCharacterWheel_GetMenuCharacterIndex "a1????????8b0c??f30f104c"; // 8 bytes, moves a data ptr into EAX, then the value into ECX, using EBX

libzhl/functions/MenuCharacter.zhl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,18 @@ struct Menu_Character depends (ANM2, CompletionWidget) {
4242
inline ANM2* GetSeedEntrySprite() { return &this->_SeedEntrySprite; }
4343
inline ANM2* GetPageSwapWidgetSprite() { return &this->_PageSwapWidgetSprite; }
4444
inline ANM2* GetTaintedBGDecoSprite() { return &this->_TaintedMenuBGDecoSprite; }
45-
46-
inline int GetNumCharacters() { return this->_numCharacters; }
45+
46+
inline int GetNumCharacters() { return this->_numCharacters; }
4747
inline int GetSelectedCharacterMenu() { return this->_characterMenuShown; }
48+
49+
// Given a CharacterMenu ID, returns the corresponding PlayerType in the specified menu (-1 for random).
50+
static int LIBZHL_API Menu_Character::GetPlayerTypeFromMenuID(int menuID, bool taintedMenu);
51+
52+
// Given a CharacterMenu ID, returns the corresponding PlayerType in the current menu (-1 for random).
53+
int LIBZHL_API Menu_Character::GetPlayerTypeFromCurrentMenuID(int menuID);
54+
55+
// Returns the PlayerType of the currently selected character in the wheel (-1 for random).
56+
int LIBZHL_API Menu_Character::GetSelectedPlayerType();
4857
}}
4958
int Status : 0x0; //1 seeds screen //4 transitioning to tainted
5059
bool IsCharacterUnlocked : 0x5;

libzhl/functions/Seeds.zhl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ __thiscall bool Seeds::HasSeedEffect(uint32_t seedEffect);
1616
"558bec83ec1453568b71":
1717
__thiscall bool Seeds::AchievementUnlocksDisallowed();
1818

19-
// sorry for the mess, this is a direct ghidra export :(
19+
// Please note that the constructors for this class allocate 0x14 bytes for SeedEffects.
20+
// We cannot properly free this memory ourselves at the moment. The proper destructor for this
21+
// class may have been inlined everywhere. Passing the Seeds to certain functions like
22+
// StartNewGame will handle the cleanup for us however, just be mindful.
2023
struct Seeds depends (RNG,ANM2){
2124
bool _isCustomRun : 0x0;
2225
unsigned int _gameStartSeed : 0x4;
2326
RNG _rng : 0x8;
2427
unsigned int _stageSeeds[14] : 0x18;
2528
unsigned int _playerInitSeed : 0x50;
26-
unsigned int _unkseed : 0x54;
29+
// 0x54 is a pointer to some kind of data structure for SeedEffects.
2730
int _seedEffectsCount : 0x58;
2831
} : 0x5c;

repentogon/LuaInterfaces/LuaIsaac.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,18 @@ LUA_FUNCTION(Lua_StartNewGame) {
197197
int pltype = (int)luaL_optinteger(L, 1, 0);
198198
int challenge = (int)luaL_optinteger(L, 2, 0);
199199
unsigned int difficulty = (int)luaL_optinteger(L, 3, 0);
200-
unsigned int seed = (int)luaL_optinteger(L, 4, 0);
201-
Seeds seedobj; //am avoiding heap corruption this way
202-
seedobj.constructor();
203-
seedobj.set_start_seed(seed);
200+
// Note: At the moment we cannot properly free some of the memory allocated by the Seeds constructors ourselves.
201+
// However, StartNewGame will handle it for us. Just be aware of this.
202+
Seeds seedobj;
203+
if (lua_type(L, 4) == LUA_TUSERDATA) {
204+
seedobj.construct_from_copy(lua::GetLuabridgeUserdata<Seeds*>(L, 4, lua::Metatables::SEEDS, "Seeds"));
205+
} else {
206+
unsigned int seed = (unsigned int)luaL_optinteger(L, 4, 0);
207+
bool isCustomRun = lua::luaL_optboolean(L, 5, false);
208+
seedobj.constructor();
209+
seedobj.set_start_seed(seed);
210+
seedobj._isCustomRun = isCustomRun;
211+
}
204212
g_Manager->StartNewGame(pltype, challenge, seedobj, difficulty);
205213
return 0;
206214
}
@@ -784,6 +792,8 @@ LUA_FUNCTION(Lua_StartDailyGame) {
784792
const unsigned int date = (unsigned int)luaL_checkinteger(L, 1);
785793

786794
// defer start to the manager
795+
// Note: At the moment we cannot properly free some of the memory allocated by the Seeds constructors by ourselves.
796+
// However, StartNewGame will handle it for us. Just be aware of this.
787797
Seeds seeds; seeds.constructor();
788798
g_Manager->StartNewGame(ePlayerType::PLAYER_ISAAC, eChallenge::CHALLENGE_NULL, seeds, 0);
789799

repentogon/LuaInterfaces/Menus/LuaCharacterMenu.cpp

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -301,27 +301,14 @@ LUA_FUNCTION(lua_CharMenu_SetSelectedCharacterID)
301301
return 0;
302302
}
303303

304-
// Given a CharacterMenu character ID, returns the corresponding PlayerType.
305-
int GetPlayerTypeFromCharacterMenuID(const int charID, const bool taintedMenu) {
306-
if (charID > 0) {
307-
if (charID < 18) {
308-
return __ptr_g_MenuCharacterEntries[taintedMenu ? (charID + 18) : charID].playerType;
309-
} else if (charID < (int)g_ModCharacterMap.size() + 18) {
310-
const auto& modChar = g_ModCharacterMap[charID - 18];
311-
return taintedMenu ? modChar.tainted : modChar.normal;
312-
}
313-
}
314-
return -1;
315-
}
316-
317304
LUA_FUNCTION(lua_CharMenu_GetPlayerTypeFromCharacterMenuID)
318305
{
319306
lua::LuaCheckMainMenuExists(L, lua::metatables::CharacterMenuMT);
320307
Menu_Character* menu = g_MenuManager->GetMenuCharacter();
321308

322309
const int charID = (int)luaL_checkinteger(L, 1);
323310
const bool taintedMenu = lua::luaL_optboolean(L, 2, menu->GetSelectedCharacterMenu() == 1);
324-
const int playerType = GetPlayerTypeFromCharacterMenuID(charID, taintedMenu);
311+
const int playerType = Menu_Character::GetPlayerTypeFromMenuID(charID, taintedMenu);
325312

326313
if (playerType < 0) {
327314
lua_pushnil(L);
@@ -337,8 +324,7 @@ LUA_FUNCTION(lua_CharMenu_GetSelectedCharacterPlayerType)
337324
lua::LuaCheckMainMenuExists(L, lua::metatables::CharacterMenuMT);
338325
Menu_Character* menu = g_MenuManager->GetMenuCharacter();
339326

340-
const bool taintedMenu = menu->GetSelectedCharacterMenu() == 1;
341-
const int playerType = GetPlayerTypeFromCharacterMenuID(menu->SelectedCharacterID, taintedMenu);
327+
const int playerType = menu->GetSelectedPlayerType();
342328

343329
if (playerType < 0) {
344330
lua_pushnil(L);

repentogon/Patches/ASMPatches/ASMCallbacks.cpp

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,9 +1518,8 @@ void RunRenderCharacterWheelCallbacks(ANM2* sprite, Vector* pos, const int playe
15181518

15191519
void __stdcall RenderVanillaCharacterWheelTrampoline(const int menuid, Vector* pos) {
15201520
ANM2* sprite = &g_MenuManager->GetMenuCharacter()->_CharacterPortraitsSprite;
1521-
const bool taintedMenu = g_MenuManager->GetMenuCharacter()->GetSelectedCharacterMenu() == 1;
1522-
if (menuid >= 0 && menuid < 18) {
1523-
const int playerType = (menuid == 0) ? -1 : __ptr_g_MenuCharacterEntries[taintedMenu ? (menuid + 18) : menuid].playerType;
1521+
const int playerType = g_MenuManager->GetMenuCharacter()->GetPlayerTypeFromCurrentMenuID(menuid);
1522+
if (playerType < NUM_PLAYER_TYPES) {
15241523
RunRenderCharacterWheelCallbacks(sprite, pos, playerType);
15251524
} else {
15261525
// Failsafe
@@ -1637,13 +1636,11 @@ void RunPostRenderCharacterMenuCallback(const int playerType, Vector* pos, ANM2*
16371636
static bool _skipEdenTokensRender = false;
16381637

16391638
void __stdcall RenderVanillaCharacterMenuTrampoline(Vector* pos) {
1640-
int menuid = g_MenuManager->GetMenuCharacter()->SelectedCharacterID;
16411639
ANM2* vanillaSprite = g_MenuManager->GetMenuCharacter()->GetBigCharPageSprite();
16421640
Vector zeroVector(0, 0);
16431641
_skipEdenTokensRender = false;
1644-
if (menuid >= 0 && menuid < 18) {
1645-
const bool taintedMenu = g_MenuManager->GetMenuCharacter()->GetSelectedCharacterMenu() == 1;
1646-
const int playerType = (menuid == 0) ? -1 : __ptr_g_MenuCharacterEntries[taintedMenu ? (menuid + 18) : menuid].playerType;
1642+
const int playerType = g_MenuManager->GetMenuCharacter()->GetSelectedPlayerType();
1643+
if (playerType < NUM_PLAYER_TYPES) {
16471644
if (RunPreRenderCharacterMenuCallback(playerType, pos, vanillaSprite, nullptr, false)) {
16481645
vanillaSprite->Render(pos, &zeroVector, &zeroVector);
16491646
RunPostRenderCharacterMenuCallback(playerType, pos, vanillaSprite, nullptr, false, false);
@@ -1742,6 +1739,49 @@ void ASMPatchRenderCustomCharacterMenu_Default() {
17421739
sASMPatcher.PatchAt(patchAddr, &patch);
17431740
}
17441741

1742+
// MC_CAN_SELECT_CHARACTER
1743+
bool RunCanSelectCharacterCallback(const int playerType, const bool isBeingSelected) {
1744+
const int callbackid = 1328;
1745+
if (CallbackState.test(callbackid - 1000)) {
1746+
lua_State* L = g_LuaEngine->_state;
1747+
lua::LuaStackProtector protector(L);
1748+
1749+
lua_rawgeti(L, LUA_REGISTRYINDEX, g_LuaEngine->runCallbackRegistry->key);
1750+
1751+
lua::LuaResults result = lua::LuaCaller(L).push(callbackid)
1752+
.push(playerType)
1753+
.push(playerType)
1754+
.push(isBeingSelected)
1755+
.call(1);
1756+
1757+
if (!result && lua_isboolean(L, -1) && !lua_toboolean(L, -1)) {
1758+
return false;
1759+
}
1760+
}
1761+
1762+
return true;
1763+
}
1764+
1765+
bool __stdcall CharacterMenuPreSelectCharacterTrampoline() {
1766+
const int playerType = g_MenuManager->GetMenuCharacter()->GetSelectedPlayerType();
1767+
return RunCanSelectCharacterCallback(playerType, true);
1768+
}
1769+
void ASMPatchCharacterMenuPreSelectCharacter() {
1770+
void* patchAddr = (char*)sASMDefinitionHolder->GetDefinition(&AsmDefinitions::MenuCharacter_PreSelectCharacter) + 0x2;
1771+
const int8_t jump = *(int8_t*)((char*)patchAddr - 0x1);
1772+
1773+
ASMPatch::SavedRegisters reg(ASMPatch::SavedRegisters::GP_REGISTERS_STACKLESS, true);
1774+
ASMPatch patch;
1775+
patch.PreserveRegisters(reg)
1776+
.AddInternalCall(CharacterMenuPreSelectCharacterTrampoline)
1777+
.AddBytes("\x84\xC0") // TEST AL, AL
1778+
.RestoreRegisters(reg)
1779+
.AddConditionalRelativeJump(ASMPatcher::CondJumps::JZ, (char*)patchAddr + jump) // Jump for false
1780+
.AddBytes(ByteBuffer().AddAny((char*)patchAddr, 0x5)) // Restore the push we overwrote
1781+
.AddRelativeJump((char*)patchAddr + 0x5); // Continue for true
1782+
sASMPatcher.PatchAt(patchAddr, &patch);
1783+
}
1784+
17451785
// Historically, MC_PRE_MOD_UNLOAD also runs during game shutdown, when the LuaEngine is being destroyed.
17461786
// However, this happens late into the shutdown process, after Game and Manager are already destroyed,
17471787
// so any code running on this callback at this time is very likely to crash the game.
@@ -2262,4 +2302,5 @@ void ASMCallbacks::detail::ApplyPatches()
22622302
ASMPatchRenderVanillaCharacterMenu_EdenTokens();
22632303
ASMPatchRenderCustomCharacterMenu_Custom();
22642304
ASMPatchRenderCustomCharacterMenu_Default();
2305+
ASMPatchCharacterMenuPreSelectCharacter();
22652306
}

repentogon/Patches/ASMPatches/ASMCallbacks.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
bool __stdcall RunPreLaserCollisionCallback(Entity_Laser* laser, Entity* entity);
66
void RunRenderCharacterWheelCallbacks(ANM2* sprite, Vector* pos, int playerType);
7+
bool RunCanSelectCharacterCallback(int playerType, bool isBeingSelected);
78

89
void PatchPreSampleLaserCollision();
910
void PatchPreLaserCollision();

repentogon/Patches/CompletionTracker.cpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -947,12 +947,8 @@ HOOK_METHOD(PauseScreen, Render, () -> void) {
947947
HOOK_METHOD(Menu_Character, Render, () -> void) {
948948
super();
949949
CompletionWidget* cmpl = this->GetCompletionWidget();
950-
int menucharid = this->SelectedCharacterID;
951-
if (menucharid > 17 && menucharid < (int)g_ModCharacterMap.size() + 18) {
952-
const auto& menuchar = g_ModCharacterMap[menucharid - 18];
953-
const bool taintedmenu = g_MenuManager->GetMenuCharacter()->GetSelectedCharacterMenu() == 1;
954-
const int playertype = taintedmenu ? menuchar.tainted : menuchar.normal;
955-
950+
const int playertype = g_MenuManager->GetMenuCharacter()->GetSelectedPlayerType();
951+
if (playertype >= NUM_PLAYER_TYPES) {
956952
Vector* ref = &g_MenuManager->_ViewPosition;
957953
// Vector* cpos = new Vector(ref->x - 80, ref->y + 894); //goes unused
958954
ANM2* anm = cmpl->GetANM2();

0 commit comments

Comments
 (0)