├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── CMakeLists.txt ├── README.md ├── mod.json.in └── src ├── ShulkerRenderer.cpp ├── ShulkerRenderer.h ├── dllmain.cpp └── dllmain.h /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Publish Version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | MOD_NAME: Better-Inventory 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | # Clone the current repository 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | # Clone AmethystAPI into /amethyst 19 | - name: Checkout Amethyst 20 | uses: actions/checkout@v2 21 | with: 22 | repository: 'FrederoxDev/Amethyst' 23 | path: 'amethyst' 24 | 25 | # Install required tools 26 | - name: Install NASM and GH 27 | run: | 28 | choco install nasm -y 29 | choco install gh -y 30 | shell: cmd 31 | 32 | - name: Setup Visual studio 33 | uses: microsoft/setup-msbuild@v2 34 | 35 | # Bump up the version number 36 | - name: Extract and increment version number 37 | id: increment_version 38 | run: | 39 | $filePath = "CMakeLists.txt" 40 | $version_line = Select-String -Path $filePath -Pattern 'set\(MOD_VERSION "(.*)"\)' 41 | if ($version_line -match 'set\(MOD_VERSION "(\d+)\.(\d+)\.(\d+)"\)') { 42 | $major = [int]$matches[1] 43 | $minor = [int]$matches[2] 44 | $patch = [int]$matches[3] 45 | 46 | # Increment the minor version 47 | $new_patch = $patch + 1 48 | $new_version = "$major.$minor.$new_patch" 49 | 50 | # Update the CMakeLists.txt file 51 | (Get-Content $filePath) -replace 'set\(MOD_VERSION ".*"\)', "set(MOD_VERSION `"$new_version`")" | Set-Content $filePath 52 | 53 | echo "NEW_VERSION=$new_version" >> $env:GITHUB_ENV 54 | echo "FILE_PATH=$filePath" >> $env:GITHUB_ENV 55 | } else { 56 | Write-Error "Version line not found or does not match the expected format." 57 | exit 1 58 | } 59 | shell: pwsh 60 | 61 | - name: Push bumped version 62 | run: | 63 | git config user.name "github-actions[bot]" 64 | git config user.email "github-actions[bot]@users.noreply.github.com" 65 | git add $env:FILE_PATH 66 | git commit -m "Bump version to $env:NEW_VERSION" 67 | git push 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | shell: pwsh 71 | 72 | - name: Build Mod 73 | run: | 74 | mkdir build 75 | cd build 76 | cmake -DCI_CD_BUILD=ON .. 77 | msbuild ${{ env.MOD_NAME }}.sln 78 | 79 | - name: Package Build 80 | run: | 81 | $version = $env:NEW_VERSION 82 | $sourcePath = "dist/${{ env.MOD_NAME }}@$version" 83 | $zipPath = "dist/${{ env.MOD_NAME }}@$version.zip" 84 | 85 | if (-Not (Test-Path -Path $sourcePath)) { 86 | Write-Error "Source path does not exist: $sourcePath" 87 | exit 1 88 | } 89 | 90 | Add-Type -AssemblyName System.IO.Compression.FileSystem 91 | [System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath, $zipPath) 92 | shell: pwsh 93 | 94 | - name: Create GitHub Release 95 | id: create_release 96 | run: | 97 | $tag_name = "v$env:NEW_VERSION" 98 | $release_name = "Release $env:NEW_VERSION" 99 | gh release create $tag_name --title "$release_name" --notes "Automated release" --target main --repo ${{ github.repository }} --draft=false --prerelease=false 100 | shell: pwsh 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | 104 | - name: Upload Release Asset 105 | run: | 106 | $asset_path = "dist/${{ env.MOD_NAME }}@$env:NEW_VERSION.zip" 107 | $asset_label = "${{ env.MOD_NAME }}@$env:NEW_VERSION.zip" 108 | gh release upload "v$env:NEW_VERSION" "$asset_path#$asset_label" --repo ${{ github.repository }} 109 | shell: pwsh 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "iostream": "cpp" 4 | } 5 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(Better-Inventory CXX ASM_NASM) 3 | set(MOD_VERSION "1.6.1") 4 | set(MOD_AUTHOR "FrederoxDev") 5 | 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /await") 8 | 9 | # Amethyst Minecraft Folder 10 | if (CI_CD_BUILD) 11 | configure_file(mod.json.in "${CMAKE_SOURCE_DIR}/dist/${PROJECT_NAME}@${MOD_VERSION}/mod.json" @ONLY) 12 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_SOURCE_DIR}/dist/${PROJECT_NAME}@${MOD_VERSION}") 13 | set(AMETHYST_SRC "${CMAKE_SOURCE_DIR}/amethyst") 14 | else() 15 | set(AmethystFolder "$ENV{localappdata}/Packages/Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState/games/com.mojang/amethyst/") 16 | configure_file(mod.json.in "${AmethystFolder}/mods/${PROJECT_NAME}@${MOD_VERSION}/mod.json" @ONLY) 17 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${AmethystFolder}/mods/${PROJECT_NAME}@${MOD_VERSION}") 18 | set(AMETHYST_SRC "$ENV{amethyst_src}/amethyst") 19 | endif() 20 | 21 | # Define only RelWithDebInfo as the available build configuration 22 | set(CMAKE_CXX_STANDARD 23) 23 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 24 | set(CMAKE_CONFIGURATION_TYPES "RelWithDebInfo" CACHE STRING "Build configurations" FORCE) 25 | set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build, options are: RelWithDebInfo" FORCE) 26 | 27 | # Project Files 28 | file(GLOB_RECURSE ${PROJECT_NAME}_All 29 | ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp 30 | ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c 31 | ${CMAKE_CURRENT_SOURCE_DIR}/src/*.asm 32 | ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp 33 | ${CMAKE_CURRENT_SOURCE_DIR}/src/*.h 34 | ) 35 | 36 | add_library(${PROJECT_NAME} SHARED ${${PROJECT_NAME}_All}) 37 | 38 | # Pass mod options to the source code 39 | target_compile_definitions(${PROJECT_NAME} PRIVATE MOD_VERSION="${MOD_VERSION}") 40 | target_compile_definitions(${PROJECT_NAME} PRIVATE MOD_AUTHOR="${MOD_AUTHOR}") 41 | target_compile_definitions(${PROJECT_NAME} PRIVATE MOD_NAME="${PROJECT_NAME}") 42 | 43 | # Link libraries 44 | target_link_libraries(${PROJECT_NAME} PRIVATE 45 | AmethystAPI 46 | libhat 47 | ${AMETHYST_SRC}/AmethystAPI/lib/fmt.lib 48 | ) 49 | 50 | # Enable multi processor compilation for C++, to make it go brrrrrr 51 | target_compile_options(${PROJECT_NAME} PRIVATE 52 | $<$:/MP> 53 | ) 54 | 55 | # EnTT Config Options 56 | target_compile_definitions(${PROJECT_NAME} PUBLIC ENTT_PACKED_PAGE=128) 57 | 58 | # Get the AmethystAPI Lib 59 | add_subdirectory("${AMETHYST_SRC}/AmethystAPI" "build") 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Inventory 2 | 3 | Better Inventory is an inventory improvement mod for Minecraft Bedrock edition, version `1.21.0.3`. The mod is built with [Amethyst](https://github.com/FrederoxDev/Amethyst) and it adds in a shulker box preview, as well as the ability to see extra information about other items, like its identifier, namespace, durability and aux id. 4 | 5 | ### Shulker box previewer 6 | ![image](https://github.com/FrederoxDev/Better-Inventory/assets/69014593/a6f26fd7-f934-4a9a-95ba-5f03eb950509) 7 | 8 | ### Item Information 9 | ![image](https://github.com/FrederoxDev/Better-Inventory/assets/69014593/97290890-1a12-4c61-a9ac-407bf78289d6) 10 | -------------------------------------------------------------------------------- /mod.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "name": "@PROJECT_NAME@", 4 | "version": "@MOD_VERSION@", 5 | "author": "@MOD_AUTHOR@" 6 | } 7 | } -------------------------------------------------------------------------------- /src/ShulkerRenderer.cpp: -------------------------------------------------------------------------------- 1 | #include "ShulkerRenderer.h" 2 | #include 3 | #include 4 | #include 5 | 6 | extern ItemStack shulkerInventory[SHULKER_CACHE_SIZE][27]; 7 | 8 | // Texture loading 9 | static HashedString flushString(0xA99285D21E94FC80, "ui_flush"); 10 | 11 | bool hasLoadedTexture = false; 12 | 13 | // Slot sizing 14 | float slotSize = 20.f; 15 | float borderSize = (slotSize - 16.f) / 2; 16 | 17 | // Uv positions 18 | glm::tvec2 itemSlotUvPos(188.0f / 256.0f, 184.0f / 256.0f); 19 | glm::tvec2 itemSlotUvSize(22.0f / 256.0f, 22.0f / 256.0f); 20 | Amethyst::NinesliceHelper backgroundNineslice(16, 16, 4, 4); 21 | 22 | int countNewlines(const std::string& str) { 23 | int newlineCount = 0; 24 | 25 | for (char c : str) { 26 | if (c == '\n') { 27 | newlineCount++; 28 | } 29 | } 30 | 31 | return newlineCount; 32 | } 33 | 34 | void ShulkerRenderer::Render(UIRenderContext* ctx, HoverRenderer* hoverRenderer, int index) { 35 | // Only load inventory resources once 36 | if (!hasLoadedTexture) { 37 | mBackgroundTexture = ctx->getTexture("textures/ui/purpleBorder", true); 38 | mItemSlotTexture = ctx->getTexture("textures/gui/gui", true); 39 | 40 | hasLoadedTexture = true; 41 | } 42 | 43 | float textHeight = (countNewlines(hoverRenderer->mFilteredContent) + 1) * 10.0f; 44 | float panelWidth = slotSize * 9; 45 | float panelHeight = slotSize * 3 + textHeight; 46 | 47 | float panelX = hoverRenderer->mCursorPosition.x + hoverRenderer->mOffset.x; 48 | float panelY = hoverRenderer->mCursorPosition.y + hoverRenderer->mOffset.y; 49 | 50 | // Draw the background panel 51 | RectangleArea background = {panelX - 4, panelX + panelWidth + 4, panelY - 4, panelY + panelHeight + 4}; 52 | backgroundNineslice.Draw(background, &mBackgroundTexture, ctx); 53 | ctx->flushImages(mce::Color::WHITE, 1.0f, flushString); 54 | 55 | // Draw the item slots 56 | for (int x = 0; x < 9; x++) { 57 | for (int y = 0; y < 3; y++) { 58 | glm::tvec2 size(slotSize, slotSize); 59 | glm::tvec2 position(panelX + slotSize * x, panelY + textHeight + slotSize * y); 60 | 61 | ctx->drawImage(mItemSlotTexture, &position, &size, &itemSlotUvPos, &itemSlotUvSize, 0); 62 | } 63 | } 64 | 65 | // It's possible to tint the background here 66 | ctx->flushImages(mce::Color::WHITE, 1.0f, flushString); 67 | 68 | // Draw the item icons 69 | BaseActorRenderContext renderCtxPtr = BaseActorRenderContext(ctx->mScreenContext, ctx->mClient, ctx->mClient->minecraftGame); 70 | 71 | for (int x = 0; x < 9; x++) { 72 | for (int y = 0; y < 3; y++) { 73 | const ItemStack* itemStack = &shulkerInventory[index][y * 9 + x]; 74 | if (itemStack->mItem == nullptr) continue; 75 | 76 | float xPos = (x * slotSize) + borderSize + panelX; 77 | float yPos = (y * slotSize) + borderSize + textHeight + panelY; 78 | 79 | renderCtxPtr.itemRenderer->renderGuiItemNew(&renderCtxPtr, itemStack, 0, xPos, yPos, false, 1.f, 1.f, 1.f); 80 | } 81 | } 82 | 83 | ctx->flushImages(mce::Color::WHITE, 1.0f, flushString); 84 | 85 | // Draw Item Counts 86 | TextMeasureData textData; 87 | memset(&textData, 0, sizeof(TextMeasureData)); 88 | textData.fontSize = 1.0f; 89 | 90 | CaretMeasureData caretData; 91 | memset(&caretData, 1, sizeof(CaretMeasureData)); 92 | 93 | // Draw the item counts 94 | for (int x = 0; x < 9; x++) { 95 | for (int y = 0; y < 3; y++) { 96 | ItemStack* itemStack = &shulkerInventory[index][y * 9 + x]; 97 | if (itemStack->mItem == nullptr) continue; 98 | if (itemStack->mCount == 1) continue; 99 | 100 | float top = (y * slotSize) + borderSize + textHeight + panelY; 101 | float bottom = top + 16.f; 102 | 103 | float left = (x * slotSize) + borderSize + panelX; 104 | float right = left + 16.f; 105 | 106 | std::string text = fmt::format("{}", itemStack->mCount); 107 | RectangleArea rect = { left, right, top + 7.f, bottom}; 108 | 109 | ctx->drawDebugText(&rect, &text, &mce::Color::WHITE, 1.0f, ui::Left, &textData, &caretData); 110 | } 111 | } 112 | 113 | ctx->flushText(0.0f); 114 | 115 | RectangleArea textArea = { panelX, panelX + panelWidth, panelY, panelY + textHeight }; 116 | RectangleArea rect = { panelX, panelX + panelWidth, panelY, panelY + panelHeight }; 117 | ctx->drawDebugText(&textArea, &hoverRenderer->mFilteredContent, &mce::Color::WHITE, 1.0f, ui::Left, &textData, &caretData); 118 | ctx->flushText(0.0f); 119 | } -------------------------------------------------------------------------------- /src/ShulkerRenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #define SHULKER_CACHE_SIZE 16 14 | 15 | class ShulkerRenderer { 16 | private: 17 | mce::TexturePtr mBackgroundTexture; 18 | mce::TexturePtr mItemSlotTexture; 19 | 20 | public: 21 | void Render(UIRenderContext* ctx, HoverRenderer* hoverRenderer, int index); 22 | }; -------------------------------------------------------------------------------- /src/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include "dllmain.h" 2 | 3 | ClientInstance* client; 4 | 5 | ShulkerRenderer shulkerRenderer; 6 | ItemStack shulkerInventory[SHULKER_CACHE_SIZE][27]; 7 | 8 | SafetyHookInline _Item_appendFormattedHoverText; 9 | SafetyHookInline _Shulker_appendFormattedHoverText; 10 | SafetyHookInline _HoverRenderer__renderHoverBox; 11 | 12 | static void Item_appendFormattedHovertext(Item* self, const ItemStackBase& itemStack, Level& level, std::string& text, uint8_t a5) { 13 | _Item_appendFormattedHoverText.call(self, itemStack, level, text, a5); 14 | 15 | Item* item = itemStack.mItem; 16 | uint64_t max = item->getMaxDamage(); 17 | 18 | if (max != 0) { 19 | uint64_t current = max - item->getDamageValue(itemStack.mUserData); 20 | text.append(fmt::format("\n{}7Durability: {} / {}{}r", "\xc2\xa7", current, max, "\xc2\xa7")); 21 | } 22 | 23 | std::string rawNameId = std::string(item->mRawNameId.c_str()); 24 | 25 | if (rawNameId == "bee_nest" || rawNameId == "beehive") { 26 | AppendBeeNestInformation(text, itemStack); 27 | } 28 | 29 | if (rawNameId.find("shulker_box") != std::string::npos) { 30 | // Don't append the id for shulker boxes (makes it too long) 31 | text.append(fmt::format("\n{}8{}:{}{}r", "\xc2\xa7", item->mNamespace, rawNameId, "\xc2\xa7")); 32 | return; 33 | } 34 | 35 | text.append(fmt::format("\n{}8{}:{} ({}){}r", "\xc2\xa7", item->mNamespace, rawNameId, item->mId, "\xc2\xa7")); 36 | } 37 | 38 | static void AppendBeeNestInformation(std::string& text, const ItemStackBase& itemStack) { 39 | CompoundTag* userData = itemStack.mUserData; 40 | 41 | // There are no bees in the bee nest 42 | if (userData == nullptr || !userData->contains("Occupants")) { 43 | text.append(fmt::format("\n{}5Contains 0 bees", "\xc2\xa7")); 44 | return; 45 | }; 46 | 47 | ListTag* occupants = userData->getList("Occupants"); 48 | text.append(fmt::format("\n{}5Contains {:d} bee{}", "\xc2\xa7", occupants->size(), occupants->size() > 1 ? "s" : "")); 49 | } 50 | 51 | int index = 0; 52 | 53 | static void Shulker_appendFormattedHovertext(ShulkerBoxBlockItem* self, const ItemStackBase& itemStack, Level& level, std::string& text, uint8_t someBool) { 54 | // Use the appendFormattedHovertext for regular items, we don't want the list of items 55 | Item_appendFormattedHovertext(self, itemStack, level, text, someBool); 56 | 57 | index++; 58 | if (index >= SHULKER_CACHE_SIZE) index = 0; 59 | 60 | // Hide a secret index in the shulker name 61 | // We do this because appendFormattedHovertext gets called for the neightboring items so if there is a shulker 62 | // to the right of this one then its preview will get overriden, so we keep track of multiple at once using a rolling identifier 63 | text += fmt::format("{}{:x}", "\xc2\xa7", index); 64 | int thisIndex = index; 65 | 66 | // Reset all the currrent item stacks 67 | for (auto& itemStack : shulkerInventory[index]) { 68 | itemStack = ItemStack(); 69 | } 70 | 71 | if (!itemStack.mUserData) return; 72 | if (!itemStack.mUserData->contains("Items")) return; 73 | 74 | const ListTag* items = itemStack.mUserData->getList("Items"); 75 | 76 | for (int i = 0; i < items->size(); i++) { 77 | const CompoundTag* itemCompound = items->getCompound(i); 78 | byte slot = itemCompound->getByte("Slot"); 79 | shulkerInventory[thisIndex][slot]._loadItem(itemCompound); 80 | } 81 | } 82 | 83 | static void HoverRenderer__renderHoverBox(HoverRenderer* self, MinecraftUIRenderContext* ctx, IClientInstance* client, RectangleArea* aabb, float someFloat) { 84 | // This is really bad code, it is relying on the fact that I have also hooked appendFormattedHovertext for items to append the item identifier 85 | // I have no idea where the currently hovered item is stored in the game! I can't find any references to it, so it might be set in some weird place? 86 | 87 | if (self->mFilteredContent.find("shulker_box") != std::string::npos) { 88 | std::string cachedIndex = self->mFilteredContent.substr(self->mFilteredContent.size() - 7, 1); 89 | 90 | try { 91 | int index = std::stoi(cachedIndex, nullptr, 16); 92 | shulkerRenderer.Render(ctx, self, index); 93 | } 94 | catch (...) { 95 | Log::Warning("There was an issue reading the shulker box! No id found, instead got: {}", cachedIndex); 96 | return; 97 | } 98 | 99 | return; 100 | } 101 | 102 | _HoverRenderer__renderHoverBox.thiscall(self, ctx, client, aabb, someFloat); 103 | } 104 | 105 | ModFunction void Initialize(AmethystContext& ctx) { 106 | Amethyst::InitializeAmethystMod(ctx); 107 | 108 | auto& hooks = Amethyst::GetHookManager(); 109 | 110 | hooks.RegisterFunction<&Item::appendFormattedHovertext>("40 55 53 56 57 41 54 41 55 41 56 41 57 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B 05 ? ? ? ? 48 33 C4 48 89 45 ? 49 8B F1 4C 89 44 24 ? 4C 8B F2 48 8B D9"); 111 | hooks.CreateHook<&Item::appendFormattedHovertext>(_Item_appendFormattedHoverText, &Item_appendFormattedHovertext); 112 | 113 | hooks.RegisterFunction<&ShulkerBoxBlockItem::appendFormattedHovertext>("40 53 55 56 57 41 56 41 57 48 81 EC ? ? ? ? 48 8B 05 ? ? ? ? 48 33 C4 48 89 44 24 ? 4D 8B F9 48 8B DA"); 114 | hooks.CreateHook<&ShulkerBoxBlockItem::appendFormattedHovertext>(_Shulker_appendFormattedHoverText, &Shulker_appendFormattedHovertext); 115 | 116 | hooks.RegisterFunction<&HoverRenderer::_renderHoverBox>("48 8B C4 48 89 58 ? 48 89 70 ? 48 89 78 ? 4C 89 70 ? 55 48 8D 68 ? 48 81 EC ? ? ? ? 0F 29 70 ? 0F 29 78 ? 44 0F 29 40 ? 49 8B D9"); 117 | hooks.CreateHook<&HoverRenderer::_renderHoverBox>(_HoverRenderer__renderHoverBox, &HoverRenderer__renderHoverBox); 118 | } -------------------------------------------------------------------------------- /src/dllmain.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "ShulkerRenderer.h" 34 | 35 | #define ModFunction extern "C" __declspec(dllexport) 36 | 37 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { 38 | return TRUE; 39 | } 40 | 41 | static void AppendBeeNestInformation(std::string& text, const ItemStackBase& itemStack); --------------------------------------------------------------------------------