-
Notifications
You must be signed in to change notification settings - Fork 42
Custom map icons support #788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| #include "IsaacRepentance.h" | ||
| #include "LuaCore.h" | ||
| #include "HookSystem.h" | ||
| #include <memory> | ||
| #include "../../Patches/CustomModManager.h" | ||
|
|
||
| /*LUA_FUNCTION(Lua_GameGetMinimap) | ||
| { | ||
|
|
@@ -12,6 +14,257 @@ | |
| } | ||
| */ | ||
|
|
||
| enum MinimapIconPosition { | ||
| ICON_POSITION_BEFORE_MAP_ICONS = 0, | ||
| ICON_POSITION_AFTER_MAP_ICONS = 1, | ||
| ICON_POSITION_BEFORE_CURSE_ICONS = 2, | ||
| ICON_POSITION_AFTER_CURSE_ICONS = 3, | ||
| }; | ||
|
|
||
| const int MAX_MINIMAP_ICONS = 7; | ||
| const int BASE_ICON_X_OFFSET = 16; | ||
| const int MAX_CURSES = 8; | ||
|
|
||
| struct MinimapIcon { | ||
| ANM2* sprite; | ||
| std_string anim; | ||
| int frame; | ||
| }; | ||
|
|
||
| struct RegisteredMinimapIcon { | ||
| ANM2 sprite; | ||
| int position; | ||
| // Unique id that's passed to the lua side | ||
| int id; | ||
| }; | ||
|
|
||
| static std::vector<RegisteredMinimapIcon*> RegisteredCustomIcons; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why use pointers to the structures when this container is supposed to be their owner?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I don't use pointers here I get access violation errors when trying to access these elements in other functions. Is there anything that im doing wrong in the struct initialization that I should change to be able to not use pointers?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not need to be pointers, you could definitely have I don't immediately know the exact reason you are getting access violations, but one possible guess I have is that you are creating a copy of your struct and the ANM2 object is not being handled properly (ie, you create a copy of the contained ANM2 when you insert into the vector, but it just copies some of the contained pointers which break when the original is deleted). While it is possible for us to make our ANM2s copyable, that's not really the point here, since you want to avoid unnecessary copies anyway. We do not need to copy an ANM2 at any point. One way to handle this, I THINK, would be to do Typically you'd create the structure directly during insertion, by either constructing inline with Functionally, to be fair, the use of a pointer here is okay since we never need to free it, but its still better practice to maintain clear ownership of data, so
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Attempted using I pinpointed the crash the rendering loop, where the pointer to the sprite is used, so I'm guessing doing
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are most likely still getting tripped up on some C++ nitty gritty somewhere along the line that is a bit hard to determine second hand. I don't believe there's a fundamental issue with the structs being owned by the vector. Maybe there is still an unintentional copy happening somewhere. Personally, I would be okay if you leave it as a pointer for now, strictly because we never need to free a registered icon. After you've finished with other code changes in this review, potentially even after we merge this in, we can take a closer look at this particular bit (I mostly just don't want to get stuck in a back-and-forth for this issue while you're making other changes, and it does work fine as-is even if not ideal) |
||
| // Set containing the indexes in RegisteredCustomIcons containing active icons | ||
| static std::set<int> ActiveCustomIcons; | ||
|
|
||
| #pragma region Helper functions | ||
| void TryAddIcon(MinimapIcon icons[], unsigned int& num_icons, ANM2* sprite, std_string& anim, int frame) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest marking all functions that do not need to be accessed outside of this file as |
||
| if (num_icons < MAX_MINIMAP_ICONS) { | ||
| icons[num_icons].sprite = sprite; | ||
| icons[num_icons].anim = anim; | ||
| icons[num_icons].frame = frame; | ||
|
|
||
| num_icons++; | ||
| } | ||
| } | ||
|
|
||
| void TryAddAllIcons(MinimapIcon icons[], unsigned int& num_icons, std::set<int>::iterator& it, int position) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could probably all this "TryAddCustomIcons", since it is specific to that |
||
| if (num_icons >= MAX_MINIMAP_ICONS) { | ||
| return; | ||
| } | ||
|
|
||
| for (it; it != ActiveCustomIcons.end(); ++it) { | ||
| int icon_idx = *it; | ||
| RegisteredMinimapIcon* icon = RegisteredCustomIcons.at(icon_idx); | ||
|
|
||
| if (icon->position <= position) { | ||
| TryAddIcon(icons, num_icons, &icon->sprite, icon->sprite._animData->_name, icon->sprite.GetFrame()); | ||
| } else { | ||
| break; | ||
| } | ||
|
|
||
| if (num_icons >= MAX_MINIMAP_ICONS) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bool ValidateSprite(ANM2& sprite, const char* anim, int& frame) { | ||
| if (!sprite._loaded) { | ||
| return false; | ||
| } | ||
|
|
||
| sprite.SetAnimation(anim, true); | ||
| AnimationData* anim_data = sprite._animData; | ||
| if (anim_data == nullptr) { | ||
| return false; | ||
| } | ||
|
|
||
| if (frame >= anim_data->GetLength()) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool CompareIcons(RegisteredMinimapIcon* a, RegisteredMinimapIcon* b) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would personally rename this function, as this specifically tells you if the map has a lower position, rather than being a three-way comparison, alternatively you could define an
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To provide examples for the alternative suggestions: Struct with less-than operator (enables direct comparison of the struct, and natural use of things like Lambda (essentially an inline function): The lambda example can be nice because then you have the logic inline within the context of the function. If you never need to do such a comparison ANYWHERE else in the code, the lambda might be a good option to take, since you don't need the function elsewhere. (Note I did not test these examples, I could have made a mistake somewhere but the general structure is correct) |
||
| return a->position < b->position; | ||
| } | ||
| #pragma endregion | ||
|
|
||
| HOOK_METHOD(Game, Exit, (bool shouldSave) -> void) { | ||
| ActiveCustomIcons.clear(); | ||
|
|
||
| super(shouldSave); | ||
| } | ||
|
|
||
| HOOK_METHOD(Minimap, RenderIcons, (Vector* position, float alpha) -> void) { | ||
| // Check if we actually need to do the custom logic. | ||
| if (ActiveCustomIcons.size() <= 0) { | ||
| super(position, alpha); | ||
| return; | ||
| } | ||
|
|
||
| PlayerManager* player_manager = g_Game->GetPlayerManager(); | ||
| RNG* rng = &player_manager->_rng; | ||
| Vector vector_zero(0, 0); | ||
|
|
||
| unsigned int num_icons = 0; | ||
| MinimapIcon icons_to_render[MAX_MINIMAP_ICONS]; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this could just be another std::vector and use .size() to check the current num_icons TryAddIcon etc would receive it as One way to add to the vector without any copying would be |
||
| Vector render_position(position->x, position->y); | ||
|
|
||
| std_string icons_anim = "icons"; | ||
| std_string curses_anim = "curses"; | ||
|
|
||
| std::set<int>::iterator active_custom_icons_it = ActiveCustomIcons.begin(); | ||
|
|
||
| #pragma region Item icons | ||
| TryAddAllIcons(icons_to_render, num_icons, active_custom_icons_it, ICON_POSITION_BEFORE_MAP_ICONS); | ||
|
|
||
| if (player_manager->FirstCollectibleOwner(COLLECTIBLE_MIND, &rng, true)) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, icons_anim, 5); | ||
| } else { | ||
| if (player_manager->FirstCollectibleOwner(COLLECTIBLE_COMPASS, &rng, true)) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, icons_anim, 0); | ||
| } | ||
|
|
||
| if (player_manager->FirstCollectibleOwner(COLLECTIBLE_BLUE_MAP, &rng, true)) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, icons_anim, 1); | ||
| } | ||
|
|
||
| if (player_manager->FirstCollectibleOwner(COLLECTIBLE_TREASURE_MAP, &rng, true)) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, icons_anim, 2); | ||
| } | ||
| } | ||
|
|
||
| TryAddAllIcons(icons_to_render, num_icons, active_custom_icons_it, ICON_POSITION_AFTER_MAP_ICONS); | ||
|
|
||
| if (player_manager->FirstCollectibleOwner(COLLECTIBLE_RESTOCK, &rng, true)) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, icons_anim, 4); | ||
| } | ||
| #pragma endregion | ||
|
|
||
| #pragma region Curse icons | ||
| TryAddAllIcons(icons_to_render, num_icons, active_custom_icons_it, ICON_POSITION_BEFORE_CURSE_ICONS); | ||
|
|
||
| unsigned int curses = g_Game->GetCurses(); | ||
|
|
||
| for (size_t i = 0; i < MAX_CURSES; i++) { | ||
| if ((curses & 1 << i) != 0) { | ||
| TryAddIcon(icons_to_render, num_icons, &this->_itemIconsSprite, curses_anim, i); | ||
| } | ||
| } | ||
|
|
||
| std::map<uint32_t, CurseSpriteData> curseSpriteMap = GetCurseSpriteMap(); | ||
|
|
||
| if (!curseSpriteMap.empty() && num_icons < MAX_MINIMAP_ICONS) { | ||
| for (const auto& entry : curseSpriteMap) { | ||
| int adjustedCurseId = (int)entry.first; | ||
| ANM2* curseSprite = entry.second.customANM2; | ||
|
|
||
| if (adjustedCurseId >= (MAX_CURSES + 1) && curseSprite->_loaded && (curses & (1U << (adjustedCurseId - 1)))) { | ||
| TryAddIcon(icons_to_render, num_icons, curseSprite, curses_anim, entry.second.frameNum); | ||
|
|
||
| if (num_icons >= MAX_MINIMAP_ICONS) break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| TryAddAllIcons(icons_to_render, num_icons, active_custom_icons_it, ICON_POSITION_AFTER_CURSE_ICONS); | ||
| #pragma endregion | ||
|
|
||
| #pragma region Rendering | ||
| Vector offset(-BASE_ICON_X_OFFSET, 0); | ||
| if (num_icons >= 4) { | ||
| offset += Vector(num_icons - 3.0f, 0); | ||
| } | ||
|
|
||
| for (size_t i = 0; i < num_icons; i++) { | ||
| MinimapIcon icon = icons_to_render[i]; | ||
|
|
||
| icon.sprite->SetFrame(&icon.anim, icon.frame); | ||
|
|
||
| icon.sprite->_color._tint[3] = alpha; | ||
| icon.sprite->Render(&render_position, &vector_zero, &vector_zero); | ||
|
|
||
| render_position += offset; | ||
| } | ||
| #pragma endregion | ||
| } | ||
|
|
||
| LUA_FUNCTION(Lua_MinimapRegisterCustomIcon) { | ||
| int position = (int)luaL_checkinteger(L, 1); | ||
| const char* anm2 = luaL_checkstring(L, 2); | ||
| std_string anm2_string(anm2); | ||
| const char* anim = luaL_checkstring(L, 3); | ||
| std_string anim_string(anim); | ||
| int frame = (int)luaL_checkinteger(L, 4); | ||
|
|
||
| RegisteredMinimapIcon* minimapIcon = new RegisteredMinimapIcon; | ||
| minimapIcon->position = position; | ||
|
|
||
| minimapIcon->sprite.Load(anm2_string, true); | ||
|
|
||
| if (!ValidateSprite(minimapIcon->sprite, anim, frame)) { | ||
| lua_pushnil(L); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For some reason I'm thinking around this validation and the potential to return nil. I'm actually not certain what I prefer here (might be OK to leave as is, just uncertain) and I am open to thoughts from both of you, something just feels slightly off to me. One other option we have would be to allow registration regardless, but if the anm2/anim/frame isn't valid, the icon won't render, or will appear to be "blank". I feel like there are other situations where messing up your anm2s will lead to things just being invisible instead of erroring outright, and perhaps this would be consistent with that. The mod creator could still tell something is up and might suspect the anm2. There's no fail state for registration itself, its just the sprite info isn't valid for rendering. Another alternative is that we return
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking back I'd remove the validation too. It's more consistent with other vanilla behaviour, as you mentioned, and it's inmediately noticeable to the user. With that in mind I don't really think that returning
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While registration-like functions are not common in the API right now, most functions that return IDs will return something like -1 for invalid things. And in general, return values being But again, maybe just removing the validation and allowing rendering to simply not work may be simpler here. |
||
| } else { | ||
| minimapIcon->sprite.SetFrame(&anim_string, frame); | ||
|
|
||
| minimapIcon->id = RegisteredCustomIcons.size(); | ||
| int index_to_place_at = 0; | ||
|
|
||
| auto lb = std::lower_bound(RegisteredCustomIcons.begin(), RegisteredCustomIcons.end(), minimapIcon, CompareIcons); | ||
| RegisteredCustomIcons.insert(lb, minimapIcon); | ||
|
|
||
| lua_pushinteger(L, minimapIcon->id); | ||
| } | ||
|
|
||
| return 1; | ||
| } | ||
|
|
||
| LUA_FUNCTION(Lua_MinimapActivateCustomIcon) { | ||
| int id = (int)luaL_checkinteger(L, 1); | ||
|
|
||
| if (id < 0 || id >= RegisteredCustomIcons.size()) { | ||
| return 0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and other Lua functions that need input validation other than type, should trigger a lua arg error, and not silently fail. |
||
| } | ||
|
|
||
| for (size_t i = 0; i < RegisteredCustomIcons.size(); i++) | ||
| { | ||
| RegisteredMinimapIcon* icon = RegisteredCustomIcons.at(i); | ||
| if (icon->id == id) { | ||
| ActiveCustomIcons.insert(i); | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| LUA_FUNCTION(Lua_MinimapDeactivateCustomIcon) { | ||
| int id = (int)luaL_checkinteger(L, 1); | ||
|
|
||
| if (id < 0 || id >= RegisteredCustomIcons.size()) { | ||
| return 0; | ||
| } | ||
|
|
||
| for (size_t i = 0; i < RegisteredCustomIcons.size(); i++) | ||
| { | ||
| RegisteredMinimapIcon* icon = RegisteredCustomIcons.at(i); | ||
| if (icon->id == id) { | ||
| ActiveCustomIcons.erase(i); | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| LUA_FUNCTION(Lua_MinimapGetState) | ||
| { | ||
| Minimap* minimap = g_Game->GetMinimap(); | ||
|
|
@@ -115,6 +368,9 @@ static void RegisterMinimap(lua_State* L) { | |
| lua::TableAssoc(L, "GetShakeOffset", Lua_MinimapGetShakeOffset); | ||
| lua::TableAssoc(L, "SetShakeOffset", Lua_MinimapSetShakeOffset); | ||
| lua::TableAssoc(L, "Refresh", Lua_MinimapRefresh); | ||
| lua::TableAssoc(L, "RegisterIcon", Lua_MinimapRegisterCustomIcon); | ||
| lua::TableAssoc(L, "ActivateIcon", Lua_MinimapActivateCustomIcon); | ||
| lua::TableAssoc(L, "DeactivateIcon", Lua_MinimapDeactivateCustomIcon); | ||
| //}; | ||
| lua_setglobal(L, "Minimap"); | ||
| //lua::RegisterNewClass(L, lua::metatables::MinimapMT, lua::metatables::MinimapMT, functions); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have full ownership of sprite, anim and frame can be removed, and instead we set the sprite to the appropriate animation and frame during validation. The render step then becomes a simple render all sprites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taking it a step further, if we do keep the MinimapIcon struct, it should just contain an ANM2 object directly, not as a pointer. No need to bother with memory management here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now i set it up as a pointer since the vanilla icons all use the same sprite. Before rendering I'm putting them all in a
MinimapIconarray. This simplifies the actual render loop, since I don't need to diferentiate between custom and vanilla icons.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could pass a pointer to the sprite when inserting them in the render queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the MinimapIcon struct is separate from the RegisteredMinimapIcon struct, then having the anm2 here as a pointer makes sense since the ANM2 is owned by either the game (for vanilla icons) or by the RegisteredMinimapIcon (for custom ones)
So this seems okay as is rn, it being generic for both vanilla and custom icons makes sense.