diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index e895ad016..9a6a63c0f 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -245,36 +245,6 @@ const char* const countMappings[] = { char itemTimestampDisplayName[TIMESTAMP_MAX][21] = { "" }; ImVec4 itemTimestampDisplayColor[TIMESTAMP_MAX]; -static const char* splitEntries[156] { - "Deku Stick", "Deku Nut", "Bombs", "Fairy Bow", "Fire Arrow", "Din's Fire", - "Slingshot", "Fairy Ocarina", "Ocarina of Time", "Bombchus", "Hookshot", "Longshot", - "Ice Arrow", "Farore's Wind", "Boomerang", "Lens of Truth", "Magic Beans", "Megaton Hammer", - "Light Arrow", "Nayru's Love", "Bottle", "Red Potion", "Green Potion", "Blue Potion", - "Fairy", "Fish", "Milk (Full)", "Ruto's Letter", "Blue Fire", "Bugs", - "Big Poe", "Milk (Half)", "Poe", "Weird Egg", "Chicken", "Zelda's Letter", - "Keaton Mask", "Skull Mask", "Spooky Mask", "Bunny Hood", "Goron Mask", "Zora Mask", - "Gerudo Mask", "Mask of Truth", "Sold Out", "Pocket Egg", "Pocket Cucco", "Cojiro", - "Odd Mushroom", "Odd Potion", "Poacher's Saw", "Broken Goron Sword", "Prescription", "Eyeball Frog", - "Eye Drops", "Claim Check", "SPLIT_BOW_ARROW_FIRE", "SPLIT_BOW_ARROW_ICE", "SPLIT_BOW_ARROW_LIGHT", "Kokiri Sword", - "Master Sword", "Biggoron's Sword", "Deku Shield", "Hylian Shield", "Mirror Shield", "Kokiri Tunic", - "Goron Tunic", "Zora Tunic", "Kokiri Boots", "Iron Boots", "Hover Boots", "Bullet Bag (30)", - "Bullet Bag (40)", "Bullet Bag (50)", "Quiver (30)", "Quiver (40)", "Quiver (50)", "Bomb Bag (20)", - "Bomb Bag (30)", "Bomb Bag (40)", "Goron's Bracelet", "Silver Gauntlets", "Gold Gauntlets", "Silver Scale", - "Golden Scale", "Giant's Knife", "Adult Wallet", "Giant's Wallet", "Seeds", "Fishing Pole", - "Minuet of Forest", "Bolero of Fire", "Serenade of Water", "Requiem of Spirit", "Nocturne of Shadow", "Prelude of Light", - "Zelda's Lullaby", "Epona's Song", "Saria's Song", "Sun's Song", "Song of Time", "Song of Storms", - "Forest Medallion", "Fire Medallion", "Water Medallion", "Spirit Medallion", "Shadow Medallion", "Light Medallion", - "Kokiri's Emerald", "Goron's Ruby", "Zora's Sapphire", "Stone of Agony", "Gerudo Membership Card", "Skulltula Token", - "Heart Container", "Piece of Heart", "Boss Key", "Compass", "Map", "Small Key", - "Magic Refill (Small)", "Magic Refill (Large)", "Piece of Heart (2)", "Magic Meter (Single)", "Magic Meter (Double)", "Double Defense", - "Null 4", "Null 5", "Null 6", "Null 7", "Milk", "Recovery Heart", - "Green Rupee", "Blue Rupee", "Red Rupee", "Purple Rupee", "Gold Rupee", "Null 8", - "Deku Sticks (5)", "Deku Sticks (10)", "Deku Nuts (5)", "Deku Nuts (10)", "Bombs (5)", "Bombs (10)", - "Bombs (20)", "Bombs (30)", "Arrows (Small)", "Arrows (Medium)", "Arrows (Large)", "Seeds (30)", - "Bombchus (5)", "Bombchus (10)", "Deku Stick Upgrade (20)", "Deku Stick Upgrade (30)", - "Deku Nut Upgrade (30)","Deku Nut Upgrade (40)", -}; - typedef struct { char name[40]; u32 time; diff --git a/soh/soh/Enhancements/timesplits/TimeSplits.cpp b/soh/soh/Enhancements/timesplits/TimeSplits.cpp index acf940dc4..df72d1813 100644 --- a/soh/soh/Enhancements/timesplits/TimeSplits.cpp +++ b/soh/soh/Enhancements/timesplits/TimeSplits.cpp @@ -5,9 +5,15 @@ #include "soh/SaveManager.h" #include "soh/util.h" #include +#include "include/z64item.h" + +#include "nlohmann-json/include/nlohmann/json.hpp" +#include +#include #include "soh/OTRGlobals.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/debugger/debugSaveEditor.h" extern "C" { extern SaveContext gSaveContext; @@ -16,7 +22,9 @@ extern "C" { // ImVec4 Colors #define COLOR_WHITE ImVec4(1.00f, 1.00f, 1.00f, 1.00f) +#define COLOR_LIGHT_RED ImVec4(1.0f, 0.05f, 0.0f, 1.0f) #define COLOR_RED ImVec4(1.00f, 0.00f, 0.00f, 1.00f) +#define COLOR_LIGHT_GREEN ImVec4(0.52f, 1.0f, 0.23f, 1.0f) #define COLOR_GREEN ImVec4(0.10f, 1.00f, 0.10f, 1.00f) #define COLOR_BLUE ImVec4(0.00f, 0.33f, 1.00f, 1.00f) #define COLOR_PURPLE ImVec4(0.54f, 0.19f, 0.89f, 1.00f) @@ -25,14 +33,21 @@ extern "C" { #define COLOR_LIGHT_BLUE ImVec4(0.00f, 0.88f, 1.00f, 1.00f) #define COLOR_GREY ImVec4(0.78f, 0.78f, 0.78f, 1.00f) -static std::vector splitItem; -static std::vector splitTime; -static std::vector splitStatus; +static std::vector splitItem; +static std::vector splitTime; +static std::vector splitBest; +static std::vector splitPreviousBest; +static std::vector splitStatus; static uint32_t itemReference = ITEM_STICK; static std::string status = ""; static ImVec4 statusColor = COLOR_WHITE; -std::time_t convertedTime; -const char* itemTime; +static uint32_t splitCurNum = 0; +std::string splitAttempt = "Attempt #: 0"; +static std::string splitNumDisp = "Attempt #: "; +static ImVec4 colorChoice = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); +static const char* backgroundColor; + +using json = nlohmann::json; std::string formatTimestampTimeSplit(uint32_t value) { uint32_t sec = value / 10; @@ -43,25 +58,18 @@ std::string formatTimestampTimeSplit(uint32_t value) { return fmt::format("{}:{:0>2}:{:0>2}.{}", hh, mm, ss, ds); } -void TimeSplitTimeHandler() { - uint32_t num = GAMEPLAYSTAT_TOTAL_TIME; - std::string temp = formatTimestampTimeSplit(num); - itemTime = temp.c_str(); -} - void TimeSplitSplitsHandler(GetItemEntry itemEntry) { if (itemEntry.modIndex != 0) { return; } uint32_t loopCounter = 0; for (auto& str : splitItem) { - uint32_t num = GAMEPLAYSTAT_TOTAL_TIME; - std::string temp = formatTimestampTimeSplit(num); - const char* timeValue = temp.c_str(); - if (itemEntry.itemId == splitItem[loopCounter]) { - splitTime[loopCounter] = timeValue; + splitTime[loopCounter] = GAMEPLAYSTAT_TOTAL_TIME; splitStatus[loopCounter] = 1; + if (splitTime[loopCounter] < splitBest[loopCounter]) { + splitBest[loopCounter] = splitTime[loopCounter]; + } } loopCounter++; } @@ -69,38 +77,61 @@ void TimeSplitSplitsHandler(GetItemEntry itemEntry) { void DrawTimeSplitSplits(){ uint32_t loopCounter = 0; + ImGui::TextColored(COLOR_YELLOW, (splitAttempt).c_str()); - ImGui::BeginTable("Splits", 4); - ImGui::TableNextColumn(); - ImGui::TextColored(COLOR_LIGHT_BLUE, "Items"); - ImGui::TableNextColumn(); - ImGui::TextColored(COLOR_LIGHT_BLUE, "Current Times"); - ImGui::TableNextColumn(); - ImGui::TextColored(COLOR_LIGHT_BLUE, "+/-"); - ImGui::TableNextColumn(); - ImGui::TextColored(COLOR_LIGHT_BLUE, "Previous Best"); + ImGui::BeginTable("Splits", 5, ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable); + ImGui::TableSetupColumn("Item Image", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel, 22.0f); + ImGui::TableSetupColumn("Item Name"); + ImGui::TableSetupColumn("Current Time", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("+/-", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Prev. Best", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableHeadersRow(); for (auto& str : splitItem) { ImGui::TableNextColumn(); - ImGui::TextColored(COLOR_WHITE, SohUtils::itemNames[splitItem[loopCounter]].c_str()); + // Item Image + ImGui::Image(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(itemImage[splitItem[loopCounter]]), ImVec2(24.0f, 24.0f), ImVec2(0, 0), ImVec2(1, 1)); ImGui::TableNextColumn(); - - if (splitStatus[loopCounter] == 0) { - uint32_t num = GAMEPLAYSTAT_TOTAL_TIME; - std::string temp = formatTimestampTimeSplit(num); - itemTime = temp.c_str(); - //TimeSplitTimeHandler(); - ImGui::TextColored(COLOR_WHITE, itemTime); + // Item Name + ImGui::Text(SohUtils::itemNames[splitItem[loopCounter]].c_str()); + ImGui::TableNextColumn(); + // Current Time + if (splitTime[loopCounter] == 0) { + ImGui::Text(formatTimestampTimeSplit(GAMEPLAYSTAT_TOTAL_TIME).c_str()); } else { - ImGui::TextColored(COLOR_GREEN, splitTime[loopCounter].c_str()); + ImGui::Text(formatTimestampTimeSplit(splitTime[loopCounter]).c_str()); } ImGui::TableNextColumn(); - // to-do: split difference +/- with colored indicators - - ImGui::TextColored(COLOR_WHITE, "Not Supported"); + // +/- + if (splitStatus[loopCounter] == 0) { + if (splitPreviousBest[loopCounter] == 0) { + ImGui::TextColored(COLOR_WHITE, formatTimestampTimeSplit(GAMEPLAYSTAT_TOTAL_TIME).c_str()); + } else { + if (GAMEPLAYSTAT_TOTAL_TIME < splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_LIGHT_GREEN, formatTimestampTimeSplit(splitPreviousBest[loopCounter] - GAMEPLAYSTAT_TOTAL_TIME).c_str()); + } else if (GAMEPLAYSTAT_TOTAL_TIME == splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_WHITE, formatTimestampTimeSplit(splitPreviousBest[loopCounter]).c_str()); + } else if (GAMEPLAYSTAT_TOTAL_TIME > splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_LIGHT_RED, formatTimestampTimeSplit(GAMEPLAYSTAT_TOTAL_TIME - splitPreviousBest[loopCounter]).c_str()); + } + } + } else { + if (splitPreviousBest[loopCounter] == 0) { + ImGui::TextColored(COLOR_GREEN, formatTimestampTimeSplit(splitTime[loopCounter]).c_str()); + } else { + if (splitTime[loopCounter] < splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_GREEN, formatTimestampTimeSplit(splitPreviousBest[loopCounter] - splitTime[loopCounter]).c_str()); + } else if (splitTime[loopCounter] == splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_WHITE, formatTimestampTimeSplit(splitTime[loopCounter] - splitPreviousBest[loopCounter]).c_str()); + } else if (splitTime[loopCounter] > splitPreviousBest[loopCounter]) { + ImGui::TextColored(COLOR_RED, formatTimestampTimeSplit(splitTime[loopCounter] - splitPreviousBest[loopCounter]).c_str()); + } + } + } ImGui::TableNextColumn(); - ImGui::Text("0:00:00.0"); - + // Previous Best + ImGui::Text(formatTimestampTimeSplit(splitPreviousBest[loopCounter]).c_str()); + loopCounter++; } ImGui::EndTable(); @@ -119,7 +150,9 @@ void DrawTimeSplitOptions() { if (ImGui::Button("Add Item")) { splitItem.push_back(itemReference); - splitTime.push_back(""); + splitTime.push_back(0); + splitPreviousBest.push_back(0); + splitBest.push_back(100000); splitStatus.push_back(0); std::string itemEntry = SohUtils::itemNames[itemReference]; status = itemEntry + std::string(" Added to List"); @@ -130,21 +163,127 @@ void DrawTimeSplitOptions() { splitItem.clear(); splitTime.clear(); splitStatus.clear(); + splitPreviousBest.clear(); status = "List has been reset"; statusColor = COLOR_RED; } ImGui::SameLine(0); + if (ImGui::Button("Save List")) { + json j; + j["splitItem"] = splitItem; + j["splitTime"] = splitTime; + j["splitPreviousBest"] = splitPreviousBest; + j["backgroundColor.r"] = colorChoice.x; + j["backgroundColor.g"] = colorChoice.y; + j["backgroundColor.b"] = colorChoice.z; + j["backgroundColor.a"] = colorChoice.w; + std::ofstream file("SplitofHarkinian.json"); + file << j.dump(4); + file.close(); + status = "List has been saved to disk"; + statusColor = COLOR_LIGHT_BLUE; + } + ImGui::SameLine(0); if (ImGui::Button("Load List")) { - + std::ifstream file("SplitofHarkinian.json"); + json j; + + if (file.is_open()) { + file >> j; + file.close(); + } + int itemSize = j["splitItem"].size(); + splitItem.clear(); + splitTime.clear(); + splitPreviousBest.clear(); + splitBest.clear(); + splitStatus.clear(); + + for (int i = 0; i < itemSize; i++) { + splitItem.push_back(0); + splitTime.push_back(0); + splitPreviousBest.push_back(0); + splitBest.push_back(100000); + splitStatus.push_back(0); + } + splitItem = j["splitItem"].get>(); + splitTime = j["splitTime"].get>(); + splitPreviousBest = j["splitPreviousBest"].get>(); + colorChoice.x = j["backgroundColor.r"]; + colorChoice.y = j["backgroundColor.g"]; + colorChoice.z = j["backgroundColor.b"]; + colorChoice.w = j["backgroundColor.a"]; + file.close(); + status = "List has been loaded from Save Data"; + statusColor = COLOR_LIGHT_BLUE; + } + if (ImGui::Button("New Attempt")) { + splitStatus.clear(); + splitTime.clear(); + for (int i = 0; i < splitItem.size(); i++) { + splitStatus.push_back(0); + splitTime.push_back(0); + } + splitCurNum++; + std::stringstream ss; + ss << splitCurNum; + splitAttempt = (splitNumDisp).c_str() + ss.str(); + DrawTimeSplitSplits(); + } + ImGui::SameLine(); + if (ImGui::Button("Update Splits")) { + for (int i = 0; i < splitItem.size(); i++) { + if (splitPreviousBest[i] == 0) { + splitPreviousBest[i] = splitBest[i]; + } else if (splitPreviousBest[i] > splitBest[i]) { + splitPreviousBest[i] = splitBest[i]; + } + } + DrawTimeSplitSplits(); + } + + if (ImGui::ColorEdit4("Background Color", (float*)&colorChoice, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + Color_RGBA8 color; + color.r = colorChoice.x; + color.g = colorChoice.y; + color.b = colorChoice.z; + color.a = colorChoice.w; + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + colorChoice = { 0.0f, 0.0f, 0.0f, 1.0f }; } ImGui::TextColored(statusColor, status.c_str()); UIWidgets::PaddedSeparator(); } +void InitializeSplitFile() { + if (!std::filesystem::exists("SplitofHarkinian.json")) { + json j; + j["splitItem"] = splitItem; + j["splitTime"] = splitTime; + j["splitPreviousBest"] = splitPreviousBest; + j["backgroundColor.r"] = colorChoice.x; + j["backgroundColor.g"] = colorChoice.y; + j["backgroundColor.b"] = colorChoice.z; + j["backgroundColor.a"] = colorChoice.w; + std::ofstream file("SplitofHarkinian.json"); + file << j.dump(4); + file.close(); + } +} + +static bool initialized = false; + void TimeSplitWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + if (!initialized) { + InitializeSplitFile(); + initialized = true; + } + ImGui::PushStyleColor(ImGuiCol_WindowBg, colorChoice); if (!ImGui::Begin("Time Splitter Window", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { ImGui::End(); + ImGui::PopStyleColor(); return; } if (ImGui::CollapsingHeader("Time Splitter")) { @@ -152,10 +291,11 @@ void TimeSplitWindow::DrawElement() { } DrawTimeSplitSplits(); ImGui::End(); + ImGui::PopStyleColor(); } void TimeSplitWindow::InitElement() { GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { - TimeSplitSplitsHandler(itemEntry); + TimeSplitSplitsHandler(itemEntry); }); } \ No newline at end of file diff --git a/soh/soh/Enhancements/timesplits/TimeSplits.h b/soh/soh/Enhancements/timesplits/TimeSplits.h index 938f812e6..5cbeafaac 100644 --- a/soh/soh/Enhancements/timesplits/TimeSplits.h +++ b/soh/soh/Enhancements/timesplits/TimeSplits.h @@ -12,4 +12,34 @@ class TimeSplitWindow : public LUS::GuiWindow { void UpdateElement() override{}; }; -#endif \ No newline at end of file +#endif + + +static const char* itemImage[156] { + "ITEM_STICK", "ITEM_NUT", "ITEM_BOMB", "ITEM_BOW", "ITEM_ARROW_FIRE", "ITEM_DINS_FIRE", + "ITEM_SLINGSHOT", "ITEM_OCARINA_FAIRY", "ITEM_OCARINA_TIME", "ITEM_BOMBCHU", "ITEM_HOOKSHOT", "ITEM_LONGSHOT", + "ITEM_ARROW_ICE", "ITEM_FARORES_WIND", "ITEM_BOOMERANG", "ITEM_LENS", "ITEM_BEAN", "ITEM_HAMMER", + "ITEM_ARROW_LIGHT", "ITEM_NAYRUS_LOVE", "ITEM_BOTTLE", "ITEM_POTION_RED", "ITEM_POTION_GREEN", "ITEM_POTION_BLUE", + "ITEM_FAIRY", "ITEM_FISH", "ITEM_MILK_BOTTLE", "ITEM_LETTER_RUTO", "ITEM_BLUE_FIRE", "ITEM_BUG", + "ITEM_BIG_POE", "ITEM_MILK_HALF", "ITEM_POE", "ITEM_WEIRD_EGG", "ITEM_CHICKEN", "ITEM_LETTER_ZELDA", + "ITEM_MASK_KEATON", "ITEM_MASK_SKULL", "ITEM_MASK_SPOOKY", "ITEM_MASK_BUNNY", "ITEM_MASK_GORON", "ITEM_MASK_ZORA", + "ITEM_MASK_GERUDO", "ITEM_MASK_TRUTH", "ITEM_SOLD_OUT", "ITEM_POCKET_EGG", "ITEM_POCKET_CUCCO", "ITEM_COJIRO", + "ITEM_ODD_MUSHROOM", "ITEM_ODD_POTION", "ITEM_SAW", "ITEM_SWORD_BROKEN", "ITEM_PRESCRIPTION", "ITEM_FROG", + "ITEM_EYEDROPS", "ITEM_CLAIM_CHECK", "ITEM_BOW_ARROW_FIRE", "ITEM_BOW_ARROW_ICE", "ITEM_BOW_ARROW_LIGHT", "ITEM_SWORD_KOKIRI", + "ITEM_SWORD_MASTER", "ITEM_SWORD_BGS", "ITEM_SHIELD_DEKU", "ITEM_SHIELD_HYLIAN", "ITEM_SHIELD_MIRROR", "ITEM_TUNIC_KOKIRI", + "ITEM_TUNIC_GORON", "ITEM_TUNIC_ZORA", "ITEM_BOOTS_KOKIRI", "ITEM_BOOTS_IRON", "ITEM_BOOTS_HOVER", "ITEM_BULLET_BAG_30", + "ITEM_BULLET_BAG_40", "ITEM_BULLET_BAG_50", "ITEM_QUIVER_30", "ITEM_QUIVER_40", "ITEM_QUIVER_50", "ITEM_BOMB_BAG_20", + "ITEM_BOMB_BAG_30", "ITEM_BOMB_BAG_40", "ITEM_BRACELET", "ITEM_GAUNTLETS_SILVER", "ITEM_GAUNTLETS_GOLD", "ITEM_SCALE_SILVER", + "ITEM_SCALE_GOLDEN", "ITEM_SWORD_KNIFE", "ITEM_WALLET_ADULT", "ITEM_WALLET_GIANT", "ITEM_SEEDS", "ITEM_FISHING_POLE", + "QUEST_SONG_MINUET", "QUEST_SONG_BOLERO", "QUEST_SONG_SERENADE", "QUEST_SONG_REQUIEM", "QUEST_SONG_NOCTURNE", "QUEST_SONG_PRELUDE", + "QUEST_SONG_LULLABY", "QUEST_SONG_EPONA", "QUEST_SONG_SARIA", "QUEST_SONG_SUN", "QUEST_SONG_TIME", "QUEST_SONG_STORMS", + "QUEST_MEDALLION_FOREST", "QUEST_MEDALLION_FIRE", "QUEST_MEDALLION_WATER", "QUEST_MEDALLION_SPIRIT", "QUEST_MEDALLION_SHADOW", "QUEST_MEDALLION_LIGHT", + "QUEST_KOKIRI_EMERALD", "QUEST_GORON_RUBY", "QUEST_ZORA_SAPPHIRE", "QUEST_STONE_OF_AGONY", "QUEST_GERUDO_CARD", "QUEST_SKULL_TOKEN", + "ITEM_HEART_CONTAINER", "ITEM_HEART_PIECE", "ITEM_KEY_BOSS", "ITEM_COMPASS", "ITEM_DUNGEON_MAP", "ITEM_KEY_SMALL", + "ITEM_MAGIC_SMALL", "ITEM_MAGIC_LARGE", "ITEM_HEART_PIECE_2", "ITEM_SINGLE_MAGIC", "ITEM_DOUBLE_MAGIC", "ITEM_DOUBLE_DEFENSE", + "ITEM_INVALID_4", "ITEM_INVALID_5", "ITEM_INVALID_6", "ITEM_INVALID_7", "ITEM_MILK", "ITEM_HEART", + "ITEM_RUPEE_GREEN", "ITEM_RUPEE_BLUE", "ITEM_RUPEE_RED", "ITEM_RUPEE_PURPLE", "ITEM_RUPEE_GOLD", "ITEM_INVALID_8", + "ITEM_STICKS_5", "ITEM_STICKS_10", "ITEM_NUTS_5", "ITEM_NUTS_10", "ITEM_BOMBS_5", "ITEM_BOMBS_10", + "ITEM_BOMBS_20", "ITEM_BOMBS_30", "ITEM_ARROWS_SMALL", "ITEM_ARROWS_MEDIUM", "ITEM_ARROWS_LARGE", "ITEM_SEEDS_30", + "ITEM_BOMBCHUS_5", "ITEM_BOMBCHUS_20", "ITEM_STICK_UPGRADE_20", "ITEM_STICK_UPGRADE_30", "ITEM_NUT_UPGRADE_30", "ITEM_NUT_UPGRADE_40" +}; \ No newline at end of file