diff --git a/README.md b/README.md index b089331..9c47608 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # FFXIV Ocean Fishing Plugin for the Elgato StreamDeck +## NOW UPDATED FOR PATCH 6.4 RUBY ROUTE +[Please refer to Installation section](#installation), **the previous installation must be removed or it won't install properly.** + StreamDeck is an external LCD key macro device that allows the installation of plugins to improve productivity. Final Fantasy XIV is a MMORPG video game. diff --git a/Release/com.elgato.ffxivoceanfishing.streamDeckPlugin b/Release/com.elgato.ffxivoceanfishing.streamDeckPlugin index 720a5ac..00a7097 100644 Binary files a/Release/com.elgato.ffxivoceanfishing.streamDeckPlugin and b/Release/com.elgato.ffxivoceanfishing.streamDeckPlugin differ diff --git a/Sources/FFXIVOceanFishingTrackerPlugin.cpp b/Sources/FFXIVOceanFishingTrackerPlugin.cpp index a91ed0d..95dee87 100644 --- a/Sources/FFXIVOceanFishingTrackerPlugin.cpp +++ b/Sources/FFXIVOceanFishingTrackerPlugin.cpp @@ -2,7 +2,7 @@ /** @file FFXIVOceanFishingTrackerPlugin.cpp @brief FFXIV Ocean Fishing Tracker plugin -@copyright (c) 2020, Momoko Tomoko +@copyright (c) 2023, Momoko Tomoko **/ //============================================================================== @@ -17,12 +17,17 @@ #include "Common/ESDConnectionManager.h" -//#define LOGGING +#define LOGGING FFXIVOceanFishingTrackerPlugin::FFXIVOceanFishingTrackerPlugin() { - mFFXIVOceanFishingHelper = new FFXIVOceanFishingHelper(); + mFFXIVOceanFishingHelper = new FFXIVOceanFishingHelper( + { + "oceanFishingDatabase - Indigo Route.json", + "oceanFishingDatabase - Ruby Route.json" + } + ); // timer that is called on certain minutes of the hour mTimer = new CallBackTimer(); @@ -81,26 +86,45 @@ void FFXIVOceanFishingTrackerPlugin::startTimers() { // First find what routes we are actually looking for. // So convert what is requested to be tracked into route IDs - std::unordered_set routeIds = mFFXIVOceanFishingHelper->getRouteIdByTracker(context.second.tracker, context.second.name); + std::unordered_set routeIds = + mFFXIVOceanFishingHelper->getVoyageIdByTracker( + context.second.routeName, + context.second.tracker, + context.second.targetName + ); // now call the helper to compute the relative time until the next window - int relativeSecondsTillNextRoute = 0; - int relativeWindowTime = 0; + uint32_t relativeSecondsTillNextRoute = 0; + uint32_t relativeWindowTime = 0; time_t startTime = time(0); - uint32_t nextRoute; - status = mFFXIVOceanFishingHelper->getSecondsUntilNextRoute(relativeSecondsTillNextRoute, relativeWindowTime, nextRoute, startTime, routeIds, context.second.skips); + status = mFFXIVOceanFishingHelper->getSecondsUntilNextVoyage( + relativeSecondsTillNextRoute, + relativeWindowTime, + startTime, + routeIds, + context.second.routeName, + context.second.skips + ); // store the absolute times context.second.routeTime = startTime + relativeSecondsTillNextRoute; context.second.windowTime = startTime + relativeWindowTime; std::string imageName; std::string buttonLabel; - mFFXIVOceanFishingHelper->getImageNameAndLabel(imageName, buttonLabel, context.second.tracker, context.second.name, context.second.priority, context.second.skips); + mFFXIVOceanFishingHelper->getImageNameAndLabel( + imageName, + buttonLabel, + context.second.routeName, + context.second.tracker, + context.second.targetName, + context.second.priority, + context.second.skips + ); if (context.second.imageName != imageName || context.second.buttonLabel != buttonLabel) context.second.needUpdate = true; // update the image - if (status && context.second.needUpdate) + if (context.second.needUpdate) { context.second.imageName = imageName; context.second.buttonLabel = buttonLabel; @@ -135,9 +159,10 @@ void FFXIVOceanFishingTrackerPlugin::UpdateUI() time_t now = time(0); for (const auto & context : mContextServerMap) { - if (context.second.name.length() > 0) + std::string titleString = ""; + if (context.second.targetName.length() > 0) { - std::string titleString = context.second.buttonLabel + "\n"; + titleString = context.second.buttonLabel + "\n"; // if we have skips, add a number to the top right if (context.second.skips != 0) { @@ -167,12 +192,11 @@ void FFXIVOceanFishingTrackerPlugin::UpdateUI() } else { - titleString += timeutils::convertSecondsToHMSString(static_cast(difftime(context.second.routeTime, now))); + titleString += "\n" + timeutils::convertSecondsToHMSString(static_cast(difftime(context.second.routeTime, now))); } - - // send the title to StreamDeck - mConnectionManager->SetTitle(titleString, context.first, kESDSDKTarget_HardwareAndSoftware); } + // send the title to StreamDeck + mConnectionManager->SetTitle(titleString, context.first, kESDSDKTarget_HardwareAndSoftware); } mVisibleContextsMutex.unlock(); } @@ -209,14 +233,19 @@ FFXIVOceanFishingTrackerPlugin::contextMetaData_t FFXIVOceanFishingTrackerPlugin mConnectionManager->LogMessage(payload.dump(4)); #endif contextMetaData_t data{}; - if (payload.find("Tracker") != payload.end()) + if (payload.find("Route") != payload.end()) + { + data.routeName = payload["Route"].get(); + } + if (payload.find("Tracker") != payload.end() && + payload["Tracker"].is_string()) { data.tracker = payload["Tracker"].get(); } if (payload.find("Name") != payload.end()) { - data.name = payload["Name"].get(); - data.buttonLabel = data.name; + data.targetName = payload["Name"].get(); + data.buttonLabel = data.targetName; data.imageName = data.imageName; } if (payload.find("DateOrTime") != payload.end()) @@ -252,6 +281,9 @@ FFXIVOceanFishingTrackerPlugin::contextMetaData_t FFXIVOceanFishingTrackerPlugin **/ void FFXIVOceanFishingTrackerPlugin::updateImage(std::string name, const std::string& inContext) { + if (name.empty()) + name = "default"; + // if image was cached, just retrieve from cache if (mImageNameToBase64Map.contains(name)) { @@ -281,26 +313,26 @@ void FFXIVOceanFishingTrackerPlugin::updateImage(std::string name, const std::st **/ void FFXIVOceanFishingTrackerPlugin::WillAppearForAction(const std::string& inAction, const std::string& inContext, const json &inPayload, const std::string& inDeviceID) { - // setup the dropdown menu by sending all possible settings + mConnectionManager->LogMessage("WillAppearForAction"); + // read payload for any saved settings, update image if needed + contextMetaData_t data{}; + if (inPayload.find("settings") != inPayload.end()) + { + data = readJsonIntoMetaData(inPayload["settings"]); + } + data.needUpdate = true; + + // setup the routes menu mInitMutex.lock(); if (mConnectionManager != nullptr && mIsInit == false) { json j; - j["menuheaders"] = mFFXIVOceanFishingHelper->getTrackerTypesJson(); - j["targets"] = mFFXIVOceanFishingHelper->getTargetsJson(); + j["routes"] = mFFXIVOceanFishingHelper->getRouteNames(); mConnectionManager->SetGlobalSettings(j); mIsInit = true; } mInitMutex.unlock(); - // read payload for any saved settings, update image if needed - contextMetaData_t data{}; - if (inPayload.find("settings") != inPayload.end()) - { - data = readJsonIntoMetaData(inPayload["settings"]); - } - data.needUpdate = true; - // if this is the first plugin to be displayed, boot up the timers if (mContextServerMap.empty()) { @@ -352,15 +384,29 @@ void FFXIVOceanFishingTrackerPlugin::SendToPlugin(const std::string& inAction, c // PI dropdown menu has saved new settings for this context, load those contextMetaData_t data; mVisibleContextsMutex.lock(); + + mConnectionManager->LogMessage("BLAH"); + + mConnectionManager->LogMessage(inPayload.dump(4)); if (mContextServerMap.find(inContext) != mContextServerMap.end()) { data = readJsonIntoMetaData(inPayload); - if (data.name != mContextServerMap.at(inContext).name || data.tracker != mContextServerMap.at(inContext).tracker) + if (data.routeName != mContextServerMap.at(inContext).routeName || + data.targetName != mContextServerMap.at(inContext).targetName || + data.tracker != mContextServerMap.at(inContext).tracker) { // update image since name changed data.needUpdate = true; } + if (data.routeName != mContextServerMap.at(inContext).routeName) + { + json j; + j["menuheaders"] = mFFXIVOceanFishingHelper->getTrackerTypesJson(data.routeName); + j["targets"] = mFFXIVOceanFishingHelper->getTargetsJson(data.routeName); + mConnectionManager->SetSettings(j, inContext); + } + // updated stored settings mContextServerMap.at(inContext) = data; } diff --git a/Sources/FFXIVOceanFishingTrackerPlugin.h b/Sources/FFXIVOceanFishingTrackerPlugin.h index 3f5b0bd..4c7077b 100644 --- a/Sources/FFXIVOceanFishingTrackerPlugin.h +++ b/Sources/FFXIVOceanFishingTrackerPlugin.h @@ -2,7 +2,7 @@ /** @file FFXIVOceanFishingTrackerPlugin.h @brief FFXIV Ocean Fishing Tracker plugin -@copyright (c) 2020, Momoko Tomoko +@copyright (c) 2023, Momoko Tomoko **/ //============================================================================== @@ -49,7 +49,8 @@ class FFXIVOceanFishingTrackerPlugin : public ESDBasePlugin // this struct contains a context's saved settings struct contextMetaData_t { - std::string name; // name of what we are tracking + std::string routeName; // name of the route we are using + std::string targetName; // name of target we are tracking std::string tracker; // tracker type, ie: blue fish or route name std::string buttonLabel; // the text on the top of the button std::string imageName; // the name of hte image to use for this button diff --git a/Sources/Resources/Shellfish_Icon.png b/Sources/Resources/Shellfish_Icon.png new file mode 100644 index 0000000..ccd62f3 Binary files /dev/null and b/Sources/Resources/Shellfish_Icon.png differ diff --git a/Sources/Resources/Shrimp_Icon.png b/Sources/Resources/Shrimp_Icon.png new file mode 100644 index 0000000..4f842bb Binary files /dev/null and b/Sources/Resources/Shrimp_Icon.png differ diff --git a/Sources/Resources/Squid_Icon.png b/Sources/Resources/Squid_Icon.png new file mode 100644 index 0000000..378943c Binary files /dev/null and b/Sources/Resources/Squid_Icon.png differ diff --git a/Sources/Resources/rescaled/Bekko_Rockhugger_Icon.png b/Sources/Resources/rescaled/Bekko_Rockhugger_Icon.png new file mode 100644 index 0000000..6386791 Binary files /dev/null and b/Sources/Resources/rescaled/Bekko_Rockhugger_Icon.png differ diff --git a/Sources/Resources/rescaled/Black-jawed_Helicoprion_Icon.png b/Sources/Resources/rescaled/Black-jawed_Helicoprion_Icon.png new file mode 100644 index 0000000..11fb3d8 Binary files /dev/null and b/Sources/Resources/rescaled/Black-jawed_Helicoprion_Icon.png differ diff --git a/Sources/Resources/rescaled/Crimson_Sentry_Icon.png b/Sources/Resources/rescaled/Crimson_Sentry_Icon.png new file mode 100644 index 0000000..efead4b Binary files /dev/null and b/Sources/Resources/rescaled/Crimson_Sentry_Icon.png differ diff --git a/Sources/Resources/rescaled/Dusk_Shark_Icon.png b/Sources/Resources/rescaled/Dusk_Shark_Icon.png new file mode 100644 index 0000000..146d9ea Binary files /dev/null and b/Sources/Resources/rescaled/Dusk_Shark_Icon.png differ diff --git a/Sources/Resources/rescaled/Fishy_Shark_Icon.png b/Sources/Resources/rescaled/Fishy_Shark_Icon.png new file mode 100644 index 0000000..0d4307f Binary files /dev/null and b/Sources/Resources/rescaled/Fishy_Shark_Icon.png differ diff --git a/Sources/Resources/rescaled/Gakugyo_Icon.png b/Sources/Resources/rescaled/Gakugyo_Icon.png new file mode 100644 index 0000000..a407443 Binary files /dev/null and b/Sources/Resources/rescaled/Gakugyo_Icon.png differ diff --git a/Sources/Resources/rescaled/Ginrin_Goshiki_Icon.png b/Sources/Resources/rescaled/Ginrin_Goshiki_Icon.png new file mode 100644 index 0000000..b2dee56 Binary files /dev/null and b/Sources/Resources/rescaled/Ginrin_Goshiki_Icon.png differ diff --git a/Sources/Resources/rescaled/Glass_Dragon_Icon.png b/Sources/Resources/rescaled/Glass_Dragon_Icon.png new file mode 100644 index 0000000..3734b04 Binary files /dev/null and b/Sources/Resources/rescaled/Glass_Dragon_Icon.png differ diff --git a/Sources/Resources/rescaled/Heavensent_Shark_Icon.png b/Sources/Resources/rescaled/Heavensent_Shark_Icon.png new file mode 100644 index 0000000..4c634ad Binary files /dev/null and b/Sources/Resources/rescaled/Heavensent_Shark_Icon.png differ diff --git a/Sources/Resources/rescaled/Hells'_Claw_Icon.png b/Sources/Resources/rescaled/Hells'_Claw_Icon.png new file mode 100644 index 0000000..ec498ca Binary files /dev/null and b/Sources/Resources/rescaled/Hells'_Claw_Icon.png differ diff --git a/Sources/Resources/rescaled/Iridescent_Trout_Icon.png b/Sources/Resources/rescaled/Iridescent_Trout_Icon.png new file mode 100644 index 0000000..5bdc04f Binary files /dev/null and b/Sources/Resources/rescaled/Iridescent_Trout_Icon.png differ diff --git a/Sources/Resources/rescaled/Jewel_of_Plum_Spring_Icon.png b/Sources/Resources/rescaled/Jewel_of_Plum_Spring_Icon.png new file mode 100644 index 0000000..f2b7878 Binary files /dev/null and b/Sources/Resources/rescaled/Jewel_of_Plum_Spring_Icon.png differ diff --git a/Sources/Resources/rescaled/Mailfish_Icon.png b/Sources/Resources/rescaled/Mailfish_Icon.png new file mode 100644 index 0000000..ab8fee8 Binary files /dev/null and b/Sources/Resources/rescaled/Mailfish_Icon.png differ diff --git a/Sources/Resources/rescaled/Mizuhiki_Icon.png b/Sources/Resources/rescaled/Mizuhiki_Icon.png new file mode 100644 index 0000000..e5667a1 Binary files /dev/null and b/Sources/Resources/rescaled/Mizuhiki_Icon.png differ diff --git a/Sources/Resources/rescaled/Pitch_Pickle_Icon.png b/Sources/Resources/rescaled/Pitch_Pickle_Icon.png new file mode 100644 index 0000000..e56751e Binary files /dev/null and b/Sources/Resources/rescaled/Pitch_Pickle_Icon.png differ diff --git a/Sources/Resources/rescaled/Shellfish_Icon.png b/Sources/Resources/rescaled/Shellfish_Icon.png new file mode 100644 index 0000000..e3dfcd3 Binary files /dev/null and b/Sources/Resources/rescaled/Shellfish_Icon.png differ diff --git a/Sources/Resources/rescaled/Shrimp_Icon.png b/Sources/Resources/rescaled/Shrimp_Icon.png new file mode 100644 index 0000000..10f9d36 Binary files /dev/null and b/Sources/Resources/rescaled/Shrimp_Icon.png differ diff --git a/Sources/Resources/rescaled/Spadefish_Icon.png b/Sources/Resources/rescaled/Spadefish_Icon.png new file mode 100644 index 0000000..7ec4cf8 Binary files /dev/null and b/Sources/Resources/rescaled/Spadefish_Icon.png differ diff --git a/Sources/Resources/rescaled/Squid_Icon.png b/Sources/Resources/rescaled/Squid_Icon.png new file mode 100644 index 0000000..1ce61ae Binary files /dev/null and b/Sources/Resources/rescaled/Squid_Icon.png differ diff --git a/Sources/Resources/rescaled/Stingfin_Trevally_Icon.png b/Sources/Resources/rescaled/Stingfin_Trevally_Icon.png new file mode 100644 index 0000000..197ee82 Binary files /dev/null and b/Sources/Resources/rescaled/Stingfin_Trevally_Icon.png differ diff --git a/Sources/Resources/rescaled/Taniwha_Icon.png b/Sources/Resources/rescaled/Taniwha_Icon.png new file mode 100644 index 0000000..a59cc30 Binary files /dev/null and b/Sources/Resources/rescaled/Taniwha_Icon.png differ diff --git a/Sources/Resources/rescaled/Un-Namazu_Icon.png b/Sources/Resources/rescaled/Un-Namazu_Icon.png new file mode 100644 index 0000000..63cc034 Binary files /dev/null and b/Sources/Resources/rescaled/Un-Namazu_Icon.png differ diff --git a/Sources/Resources/rescaled/Vivid_Pink_Shrimp_Icon.png b/Sources/Resources/rescaled/Vivid_Pink_Shrimp_Icon.png new file mode 100644 index 0000000..d761188 Binary files /dev/null and b/Sources/Resources/rescaled/Vivid_Pink_Shrimp_Icon.png differ diff --git a/Sources/Resources/rescaled/Yellow_Iris_Icon.png b/Sources/Resources/rescaled/Yellow_Iris_Icon.png new file mode 100644 index 0000000..219b41a Binary files /dev/null and b/Sources/Resources/rescaled/Yellow_Iris_Icon.png differ diff --git a/Sources/Windows/FFXIVOceanFishingHelper.cpp b/Sources/Windows/FFXIVOceanFishingHelper.cpp index f5f2f61..33e16a2 100644 --- a/Sources/Windows/FFXIVOceanFishingHelper.cpp +++ b/Sources/Windows/FFXIVOceanFishingHelper.cpp @@ -1,707 +1,162 @@ //============================================================================== /** @file FFXIVOceanFishingHelper.cpp -@brief Computes Ocean Fishing Times -@copyright (c) 2020, Momoko Tomoko +@brief Handles multiple instances of processor for various fishing routes +@copyright (c) 2023, Momoko Tomoko **/ //============================================================================== #include "pch.h" #include "FFXIVOceanFishingHelper.h" -#include -#include -#include -#include -#include "../Vendor/json/src/json.hpp" -using json = nlohmann::json; -FFXIVOceanFishingHelper::FFXIVOceanFishingHelper() - : FFXIVOceanFishingHelper(std::string("oceanFishingDatabase.json")) -{ -} - -FFXIVOceanFishingHelper::FFXIVOceanFishingHelper(const std::string& dataFile) -{ - std::ifstream ifs(dataFile); - - if (ifs.fail()) - { - errorMessage = "Failed to open datafile: " + dataFile; - return; - } - - json j; - bool jsonIsGood = false; - try - { - j = j.parse(ifs); - jsonIsGood = true; - } - catch (...) - { - errorMessage = "Failed to parse dataFile into json object."; - } - ifs.close(); - - if (jsonIsGood) - loadDatabase(j); -} -FFXIVOceanFishingHelper::FFXIVOceanFishingHelper(const json& j) +FFXIVOceanFishingHelper::FFXIVOceanFishingHelper(const std::vector& dataFiles) { - loadDatabase(j); -} - -bool FFXIVOceanFishingHelper::loadSchedule(const json& j) -{ - if (isBadKey(j, "schedule", "Missing schedule in database.")) return false; - if (isBadKey(j["schedule"], "pattern", "Missing pattern in schedule.")) return false; - if (isBadKey(j["schedule"], "offset", "Missing offset in schedule.")) return false; - - // get the pattern and store it in mRoutePattern - for (const auto& id : j["schedule"]["pattern"]) - if (id.is_number_integer()) - mRoutePattern.push_back(id.get()); - else - { - errorMessage = "Invalid pattern in schedule: " + id.dump(4) + "\n" + j["schedule"].dump(4); - return false; - } - - // get the offset and store it in mPatternOffset - if (j["schedule"]["offset"].is_number_integer()) - mPatternOffset = j["schedule"]["offset"].get(); - else + for (const auto & dataFile : dataFiles) { - errorMessage = "Invalid offset in schedule:\n" + j["schedule"].dump(4); - return false; + auto newProcessor = + std::make_unique( + FFXIVOceanFishingProcessor(dataFile) + ); + processors.insert({ newProcessor->getRouteName(), std::move(newProcessor) }); } - return true; -} - -void FFXIVOceanFishingHelper::loadDatabase(const json& j) -{ - // TODO handle parse errors - // TODO refactor this function - - // get schedule - if (!loadSchedule(j)) return; - - // get stops - for (const auto& stops : j["stops"].get()) - { - mStops.insert({ stops.first, stops.second["shortform"].get() }); - } - - // get fish - if (j["targets"].contains("fish")) - { - for (const auto& fishType : j["targets"]["fish"].get()) - { - mFishes.insert({ fishType.first, {} }); - for (const auto& fish : fishType.second.get()) - { - std::vector locations; - for (const auto& location : fish.second["locations"]) - { - // construct a vector of times the fish is available - // an empty vector means any time is allowed - - std::vector times; - if (location.contains("time")) - { - // location["time"] can be a single entry ("time": "day") or an array ("time": ["day", night"]) - if (location["time"].is_array()) - for (const auto& time : location["time"]) - times.push_back(time.get()); - else - times.push_back(location["time"].get()); - } - - locations.push_back({ location["name"].get(), times }); - } - - std::string shortformName = fish.first; // by default the shortform name is just the fish name - if (fish.second.contains("shortform")) - shortformName = fish.second["shortform"].get(); - - mFishes.at(fishType.first).insert({ fish.first, - {shortformName, - locations} - }); - if (fishType.first == "Blue Fish") - mBlueFishNames.insert({ fish.first, mFishes.at(fishType.first).at(fish.first) }); - } - } - } - - // get achievements - for (const auto& achievement : j["targets"]["achievements"].get()) - { - std::unordered_set ids; - for (const auto routeId : achievement.second["routeIds"]) - { - ids.insert(routeId.get()); - } - mAchievements.insert({achievement.first, ids}); - } - - // get routes - for (const auto& route : j["routes"].get()) - { - std::string routeName = route.first; - if (mRoutes.find(routeName) != mRoutes.end()) - { - throw std::runtime_error("Error: duplicate route name in json: " + routeName); - } - - std::vector stops; - for (const auto& stop : route.second["stops"]) - { - const std::string stopName = stop["name"].get(); - const std::string stopTime = stop["time"].get(); - std::unordered_set fishList; - // double check that the stops exist, and create list of fishes - if (mStops.find(stopName) == mStops.end()) - { - throw std::runtime_error("Error: stop " + stopName + " in route " + routeName + " does not exist in j[\"stops\"]"); - } - for (const auto& fishType : mFishes) - { - for (const auto& fish : fishType.second) - { - for (const auto& location : fish.second.locations) - { - if (location.name != stopName) - continue; - - bool isTimeMatch = false; - // empty time vector means any time is allowed - if (location.time.empty()) - isTimeMatch = true; - else - // go through each time and check for any match - for (const auto& time : location.time) - { - if (time == stopTime) - { - isTimeMatch = true; - break; - } - } - - if (!isTimeMatch) - continue; - - fishList.insert(fish.first); - break; - } - } - } - stops.push_back({ {stopName, {stopTime} } , fishList }); - } - - const uint32_t id = route.second["id"].get(); - - // load achievements into route - std::unordered_set achievements; - for (const auto& achievement : mAchievements) - { - if (achievement.second.find(id) != achievement.second.end()) - achievements.insert(achievement.first); - } - - mRoutes.insert({routeName, - { - route.second["shortform"].get(), - id, - stops, - achievements, - "" // bluefishpattern, generated later - } - }); - mRouteIdToNameMap.insert({ id, routeName }); - } - - // construct search target mapping - // targets by blue fish per route - mTargetToRouteIdMap.insert({ "Blue Fish Pattern", {} }); - for (const auto& route : mRoutes) - { - std::string blueFishPattern; - std::unordered_set blueFish; - - // create pattern string as fish1-fish2-fish3, and use X if there is no blue fish - for (const auto& stop : route.second.stops) - { - bool blueFishFound = false; - for (const auto& fish : stop.fish) // go through all the possible fishes at this stop - { - if (mBlueFishNames.find(fish) != mBlueFishNames.end()) // we only care about the blue fish - { - blueFishFound = true; - blueFishPattern += mBlueFishNames.at(fish).shortName; - blueFish.insert(fish); - break; - } - } - if (!blueFishFound) - { - blueFishPattern += "X"; - } - - blueFishPattern += "-"; - } - // remove the last dash - if (blueFishPattern.length() > 0) - blueFishPattern = blueFishPattern.substr(0, blueFishPattern.length() - 1); - - // if only 1 blue fish, use that as the image name without the Xs - std::string imageName = blueFishPattern; - if (blueFish.size() == 1) - imageName = *blueFish.begin(); - - if (blueFishPattern != "X-X-X") - { - mTargetToRouteIdMap.at("Blue Fish Pattern").insert({ blueFishPattern, - { - blueFishPattern, // label name - imageName, // image name - {} // route ids - } - }); - mTargetToRouteIdMap.at("Blue Fish Pattern").at(blueFishPattern).ids.insert(route.second.id); - mRoutes.at(route.first).blueFishPattern = blueFishPattern; - } - } - - // achievements targets: - mTargetToRouteIdMap.insert({ "Achievement", {} }); - for (const auto& achievement : mAchievements) - { - std::unordered_set ids; - for (const auto& routeId : achievement.second) - { - ids.insert(routeId); - } - mTargetToRouteIdMap.at("Achievement").insert({ achievement.first, - { - achievement.first, // achievement label and imagename are the same as just the acheivement name - achievement.first, - ids - } - }); - } - - // fish targets: - for (const auto& fishType : mFishes) - { - mTargetToRouteIdMap.insert({ fishType.first, {} }); - for (const auto& fish : fishType.second) - { - const std::string fishName = fish.first; - std::unordered_set ids; - for (const auto& route : mRoutes) - { - for (const auto& stop : route.second.stops) - { - if (stop.fish.find(fish.first) != stop.fish.end()) - { - ids.insert(route.second.id); - } - } - } - mTargetToRouteIdMap.at(fishType.first).insert({ fishName, - { - fish.first, // fish label and imagename are the same as just the fish name - fish.first, - ids - } - }); - } - } - - // targets by route name: - mTargetToRouteIdMap.insert({ "Routes", {} }); - for (const auto& route : mRoutes) - { - // create an name for this route. Priority goes to achievement, then to the route bluefishpattern - std::string name = route.first; - // TODO: This assumes only 1 achievement per route, although can have more. Right now just grab the first one and use that as the icon - if (route.second.achievements.size() != 0) - name = *route.second.achievements.begin(); - else if (route.second.blueFishPattern.length() > 0) - name = route.second.blueFishPattern; - - std::string lastStop = route.second.stops.back().location.name; - if (mStops.find(lastStop) != mStops.end()) - { - std::string lastStopShortName = mStops.at(lastStop); - - if (mTargetToRouteIdMap.find(lastStopShortName) == mTargetToRouteIdMap.end()) - mTargetToRouteIdMap.at("Routes").insert({ lastStopShortName, {"", "", {}} }); - mTargetToRouteIdMap.at("Routes").at(lastStopShortName).ids.insert(route.second.id); - } - - mTargetToRouteIdMap.at("Routes").insert({ route.first, {name, "", {route.second.id}} }); - } - - // special targets: - mTargetToRouteIdMap.insert({ "Other", {}}); - mTargetToRouteIdMap.at("Other").insert({ "Next Route", {"", "", {}} }); - - mIsInit = true; } /** - @brief gets the next route number + @brief returns the name of all processor route names as a json - @param[out] nextRoute the routeId used - @param[in] startTime the time to start counting from. - @param[in] routeIds A set of routeIds we are looking for. The closest time is returned out of all the routes. Set to empty set for any route. - @param[in] skips number of windows to skip over. Default is 0. - - @return true if successful + @return json of route names **/ -bool FFXIVOceanFishingHelper::getNextRoute(uint32_t& nextRoute, const time_t& startTime, const std::unordered_set& routeIds, const uint32_t skips) +json FFXIVOceanFishingHelper::getRouteNames() { - int relativeSecondsTillNextRoute = 0; - int relativeWindowTime = 0; - return getSecondsUntilNextRoute(relativeSecondsTillNextRoute, relativeWindowTime, nextRoute, startTime, routeIds, skips); + json j; + for (const auto& processor : processors) + j.push_back(processor.first); + return j; } /** - @brief gets the number of seconds until the next window. If already in a window, it will also get the seconds left in that window + @brief wrapper around getSecondsUntilNextVoyage for each route processor - @param[out] secondsTillNextRoute number of seconds until the next window, not including the one we are currently in + @param[out] secondsTillNextVoyage number of seconds until the next window, not including the one we are currently in @param[out] secondsLeftInWindow number of seconds left in the current window. Is set to 0 if not in a current window - @param[out] nextRoute the routeId used @param[in] startTime the time to start counting from. - @param[in] routeIds A set of routeIds we are looking for. The closest time is returned out of all the routes. Set to empty set for any route. - @param[in] skips number of windows to skip over. Default is 0. + @param[in] voyageIds A set of voyageIds we are looking for per route name. + @param[in] routeName the name of the route + @param[in] skips number of windows to skip over @return true if successful **/ -bool FFXIVOceanFishingHelper::getSecondsUntilNextRoute(int& secondsTillNextRoute, int& secondsLeftInWindow, uint32_t& nextRoute, const time_t& startTime, const std::unordered_set & routeIds, const uint32_t skips) -{ - bool nextRouteUpdated = false; - secondsLeftInWindow = 0; - - // Get the status of where we are currently - unsigned int currBlockIdx = convertTimeToBlockIndex(startTime); - - // Cycle through the route pattern until we get a match to a route we are looking for. - unsigned int skipcounts = 0; - unsigned int maxCycles = 1000; // limit cycles just in case - for (unsigned int i = 0; i < maxCycles; i++) - { - // Current place in the pattern we are looking at. - unsigned int wrappedIdx = getRoutePatternIndex(currBlockIdx, i); - - // Check to see if we match any of our desired routes - bool routeMatch = false; - if (routeIds.size() == 0) - routeMatch = true; - for (const auto& id : routeIds) - { - if (id == mRoutePattern[wrappedIdx]) - { - routeMatch = true; - break; - } - } - - if (routeMatch) - { - // Find the difference in time from the pattern position to the current time - time_t routeTime = convertBlockIndexToTime(currBlockIdx + i); - int timeDifference = static_cast(difftime(routeTime, startTime)); - - // If the time of the route is more than 15m behind us, then it's not a valid route - if (timeDifference < -60 * 15) - { - continue; - } - - // If we are skipping routes, skip now - if (skipcounts < skips) - { - skipcounts++; - continue; - } - - // If we reach here we found a valid route. - // If timeDifference <= 0, that means we are in a window, - // but we still want the time of the next route, so don't return - // and continue for another cycle to get a positive timeDifference - - if (!nextRouteUpdated) // remember the next route. Use the current window as the route if we are in window - { - nextRoute = mRoutePattern[wrappedIdx]; - nextRouteUpdated = true; - } - if (timeDifference <= 0) // needs to have the = also otherwise trigging this on the turn of the hour will not record that we're in a window - { - secondsLeftInWindow = 60 * 15 + timeDifference; - } - else - { - secondsTillNextRoute = timeDifference; - return true; - } - } - } - return false; -} - -/** - @brief gets the route name at a selected time, with option to skip. If in a window, that window is the routes name. If not, the next window will be the name. - - @param[in] t the time to start the check - @param[in] skips number of windows to skip over. Default is 0. - - @return name of the route -**/ -std::string FFXIVOceanFishingHelper::getNextRouteName(const time_t& t, const unsigned int skips) -{ - unsigned int currBlockIdx = convertTimeToBlockIndex(t); - - unsigned int skipcounts = 0; - const unsigned int maxCycles = 1000; // limit cycles just in case - for (unsigned int i = 0; i < maxCycles; i++) - { - // Find the difference in time from the pattern position to the current time - time_t routeTime = convertBlockIndexToTime(currBlockIdx + i); - int timeDifference = static_cast(difftime(routeTime, t)); - - // If the time of the route is more than 15m behind us, then it's not a valid route - if (timeDifference < -60 * 15) - { - continue; - } - - // If we are skipping routes, skip now - if (skipcounts < skips) - { - skipcounts++; - continue; - } - - // get route index - unsigned int routeIdx = mRoutePattern[getRoutePatternIndex(currBlockIdx + i)]; - - // convert from id - if (mRouteIdToNameMap.find(routeIdx) != mRouteIdToNameMap.end()) - { - const std::string routeName = mRouteIdToNameMap.at(routeIdx); - if (mRoutes.find(routeName) != mRoutes.end()) - return routeName; - } - } - return ""; -} - -/** - @brief converts a block id to a time - this algorithm was obtained from https://github.com/proyebat/FFXIVOceanFishingTimeCalculator - - @param[in] blockIdx the block index to convert - - @return the time in time_t -**/ -time_t FFXIVOceanFishingHelper::convertBlockIndexToTime(const unsigned int blockIdx) -{ - return static_cast(blockIdx - (mPatternOffset - 1)) * 60 * 60 * 2; -} - -/** - @brief converts a time to a block index - this algorithm was obtained from https://github.com/proyebat/FFXIVOceanFishingTimeCalculator - - @param[in] t the time_t to convert - - @return the block index -**/ -unsigned int FFXIVOceanFishingHelper::convertTimeToBlockIndex(const time_t& t) -{ - struct tm t_struct {}; - localtime_s(&t_struct, &t); - - // if we are within the 15 boat window, we are still inside the block. Account for this. - if (t_struct.tm_min < 15) - { - t_struct.tm_min -= 15; +bool FFXIVOceanFishingHelper::getSecondsUntilNextVoyage( + uint32_t& secondsTillNextVoyage, + uint32_t& secondsLeftInWindow, + const time_t& startTime, + const std::unordered_set& voyageIds, + const std::string& routeName, + const uint32_t skips +) +{ + if (!processors.contains(routeName)) + { + secondsTillNextVoyage = UINT_MAX; + secondsLeftInWindow = 0; + return false; } - time_t alignedTime = mktime(&t_struct); - return static_cast(std::ceil(alignedTime / (60 * 60 * 2))) + mPatternOffset; -} - -/** - @brief gets the route's id from the pattern given a block index - - @param[in] blockIdx the block to convert - @param[in] jump number of steps to jump ahead, default is 0 - @return the route id at the blockIdx + jump -**/ -unsigned int FFXIVOceanFishingHelper::getRoutePatternIndex(const unsigned int blockIdx, const unsigned int jump) -{ - return (blockIdx + jump) % mRoutePattern.size(); -} - - -/** - @brief converts a routeId to image name - - @param[in] routeId the routeId to get the image for - @param[in] priority whether to prioritize achievement name or blue fish name - - @return the image name - - @relatesalso getImageName -**/ -std::string FFXIVOceanFishingHelper::createImageNameFromRouteId(const uint32_t& routeId, PRIORITY priority) -{ - if (mRouteIdToNameMap.find(routeId) == mRouteIdToNameMap.end()) - return ""; - std::string routeName = mRouteIdToNameMap.at(routeId); - if (mRoutes.find(routeName) == mRoutes.end()) - return ""; - - std::string blueFishName = ""; - std::string blueFishPattern = mRoutes.at(routeName).blueFishPattern; - if (mTargetToRouteIdMap.find("Blue Fish Pattern") != mTargetToRouteIdMap.end() && - mTargetToRouteIdMap.at("Blue Fish Pattern").find(blueFishPattern) != mTargetToRouteIdMap.at("Blue Fish Pattern").end()) - blueFishName = mTargetToRouteIdMap.at("Blue Fish Pattern").at(blueFishPattern).imageName; - std::string achievementName = ""; - if (mRoutes.at(routeName).achievements.size() != 0) - achievementName = *mRoutes.at(routeName).achievements.begin(); // TODO: right now we assume there is only one achievement, so just return the first one - - if ((priority == ACHIEVEMENTS && !achievementName.empty()) || blueFishName.empty()) - return achievementName; - else - return blueFishName; + uint32_t returnedVoyageId; + return processors[routeName]->getSecondsUntilNextRoute( + secondsTillNextVoyage, + secondsLeftInWindow, + returnedVoyageId, // unused here + startTime, + voyageIds, // ids + skips + ); } /** - @brief converts a routeId to button label + @brief wrapper around getImageNameAndLabel for each route processor - @param[in] routeId the routeId to get the image for + @param[out] imageName the name of the png image for this tracker + @param[out] buttonLabel the string label for this button + @param[in] routeName the name of the route + @param[in] tracker the name of the tracker type (ie: Blue Fish, Achievement) + @param[in] name the name of the actual thing to track (ie: name of fish, name of Achievement) @param[in] priority whether to prioritize achievement name or blue fish name - - @return the button label - - @relatesalso getButtonLabel + @param[in] skips number of windows to skip over **/ -std::string FFXIVOceanFishingHelper::createButtonLabelFromRouteId(const uint32_t& routeId, PRIORITY priority) -{ - if (mRouteIdToNameMap.find(routeId) == mRouteIdToNameMap.end()) - return ""; - std::string routeName = mRouteIdToNameMap.at(routeId); - if (mRoutes.find(routeName) == mRoutes.end()) - return ""; - - std::string blueFishName = mRoutes.at(routeName).blueFishPattern; - std::string achievementName = ""; - if (mRoutes.at(routeName).achievements.size() != 0) - achievementName = *mRoutes.at(routeName).achievements.begin(); // TODO: right now we assume there is only one achievement, so just return the first one - - if ((priority == ACHIEVEMENTS && !achievementName.empty()) || blueFishName.empty()) - return achievementName; - else - return blueFishName; -} - -void FFXIVOceanFishingHelper::getImageNameAndLabel(std::string& imageName, std::string& buttonLabel, const std::string& tracker, const std::string& name, const PRIORITY priority, const uint32_t skips) -{ - imageName = ""; - buttonLabel = ""; - - // first look for tracker+name in the map - if (mTargetToRouteIdMap.find(tracker) != mTargetToRouteIdMap.end()) - { - if (mTargetToRouteIdMap.at(tracker).find(name) != mTargetToRouteIdMap.at(tracker).end()) - { - buttonLabel = mTargetToRouteIdMap.at(tracker).at(name).labelName; - imageName = mTargetToRouteIdMap.at(tracker).at(name).imageName; - - // if labelName or imageName not provided, we need to create them - if (buttonLabel.empty() || imageName.empty()) - { - // first get routeId for this tracker - std::unordered_set routeIds = getRouteIdByTracker(tracker, name); +void FFXIVOceanFishingHelper::getImageNameAndLabel( + std::string& imageName, + std::string& buttonLabel, + const std::string& routeName, + const std::string& tracker, + const std::string& name, + const PRIORITY priority, + const uint32_t skips +) +{ + if (!processors.contains(routeName)) + return; - // get next route - time_t startTime = time(0); - uint32_t nextRoute; - if (getNextRoute(nextRoute, startTime, routeIds, skips)) - { - // create the names - if (buttonLabel.empty()) - buttonLabel = createButtonLabelFromRouteId(nextRoute, priority); - if (imageName.empty()) - imageName = createImageNameFromRouteId(nextRoute, priority); - } - } - } - } + processors[routeName]->getImageNameAndLabel( + imageName, + buttonLabel, + tracker, + name, + priority, + skips + ); } /** @brief gets targets as json + @param[in] routeName the name of the route + @return json containing targets **/ -json FFXIVOceanFishingHelper::getTargetsJson() +json FFXIVOceanFishingHelper::getTargetsJson(const std::string& routeName) { json j; - - for (const auto& type : mTargetToRouteIdMap) - { - for (const auto& target : type.second) - { - j.emplace(target.first, type.first); - } - } + if (!processors.contains(routeName)) + return j; - return j; + return processors[routeName]->getTargetsJson(); } /** @brief gets tracker types as json + @param[in] routeName the name of the route + @return json containing tracker types **/ -json FFXIVOceanFishingHelper::getTrackerTypesJson() +json FFXIVOceanFishingHelper::getTrackerTypesJson(const std::string& routeName) { json j; + if (!processors.contains(routeName)) + return j; - for (const auto& type : mTargetToRouteIdMap) - { - j.push_back(type.first); - } - - std::sort(j.begin(), j.end()); - return j; + return processors[routeName]->getTrackerTypesJson(); } /** - @brief converts a target to a set of route ids that matches the target + @brief converts a target to a set of voyage ids that matches the target + @param[in] routeName the name of the route @param[in] type the targets type @param[in] name the targets name - @return a set of a single route id if found, and a null set if the route is not found + @return a list of voyage ids if found **/ -std::unordered_set FFXIVOceanFishingHelper::getRouteIdByTracker(const std::string& tracker, const std::string& name) +std::unordered_set FFXIVOceanFishingHelper::getVoyageIdByTracker( + const std::string& routeName, + const std::string& tracker, + const std::string& name +) { - // this map was precomputed in initializer - if (mTargetToRouteIdMap.find(tracker) != mTargetToRouteIdMap.end()) - { - if (mTargetToRouteIdMap.at(tracker).find(name) != mTargetToRouteIdMap.at(tracker).end()) - { - return mTargetToRouteIdMap.at(tracker).at(name).ids; - } - } - return {}; + if (!processors.contains(routeName)) + return {}; + + return processors[routeName]->getRouteIdByTracker(tracker, name); } \ No newline at end of file diff --git a/Sources/Windows/FFXIVOceanFishingHelper.h b/Sources/Windows/FFXIVOceanFishingHelper.h index bc45970..609fe7f 100644 --- a/Sources/Windows/FFXIVOceanFishingHelper.h +++ b/Sources/Windows/FFXIVOceanFishingHelper.h @@ -1,20 +1,17 @@ //============================================================================== /** @file FFXIVOceanFishingHelper.h -@brief Computes Ocean Fishing Times -@copyright (c) 2020, Momoko Tomoko +@brief Handles multiple instances of processor for various fishing routes +@copyright (c) 2023, Momoko Tomoko **/ //============================================================================== #pragma once +#include "FFXIVOceanFishingProcessor.h" #include -#include -#include -#include -#include #include -#include "Common.h" +#include #include "../Vendor/json/src/json.hpp" using json = nlohmann::json; @@ -22,93 +19,60 @@ using json = nlohmann::json; class FFXIVOceanFishingHelper { public: - FFXIVOceanFishingHelper(); - FFXIVOceanFishingHelper(const std::string& dataFile); - FFXIVOceanFishingHelper(const json& j); + FFXIVOceanFishingHelper(const std::vector& dataFiles); ~FFXIVOceanFishingHelper() {}; - bool isInit() { return mIsInit; }; - std::string getErrorMessage() { return errorMessage; }; - - void loadDatabase(const json& j); - - bool getNextRoute(uint32_t& nextRoute, const time_t& startTime, const std::unordered_set& routeIds, const uint32_t skips = 0); - bool getSecondsUntilNextRoute(int& secondsTillNextRoute, int& secondsLeftInWindow, uint32_t& nextRoute, const time_t& startTime, const std::unordered_set& routeIds, const uint32_t skips = 0); - std::string getNextRouteName(const time_t& t, const unsigned int skips = 0); - - std::unordered_set getRouteIdByTracker(const std::string& tracker, const std::string& name); - void getImageNameAndLabel(std::string& imageName, std::string& buttonLabel, const std::string& tracker, const std::string& name, const PRIORITY priority, const uint32_t skips); - json getTargetsJson(); - json getTrackerTypesJson(); -private: - bool mIsInit = false; - std::string errorMessage = ""; - - bool isBadKey(const json& j, const std::string& key, const std::string& msg) - { - if (!j.contains(key)) - { - errorMessage = msg + "\nJson Dump:\n" + j.dump(4); - return true; - } - return false; - } - - struct locations_t - { - const std::string name; - const std::vector time; - }; - - struct fish_t + bool isInit() { - const std::string shortName; - const std::vector locations; + for (const auto& processor : processors) + if (!processor.second->isInit()) + return false; + return true; }; - - std::unordered_map mStops; - std::unordered_map> mFishes; - std::unordered_map> mAchievements; - std::map mBlueFishNames; - - struct stop_t + std::string getErrorMessage() { - const locations_t location; - const std::unordered_set fish; - }; - - struct route_t - { - const std::string shortName; - const uint32_t id; - const std::vector stops; - const std::unordered_set achievements; - std::string blueFishPattern; - }; - std::unordered_map mRoutes; - - uint32_t mPatternOffset = 0; - std::vector mRoutePattern; + std::string err; + for (const auto& processor : processors) + { + std::string msg = processor.second->getErrorMessage(); - struct targets_t - { - const std::string labelName; - const std::string imageName; - std::unordered_set ids; + if (!msg.empty()) + err += processor.first + + " error message:\n" + + msg; + } + return err; }; - // hiearchy is target type -> target name -> struct with vector of route ids - // ie: "Blue Fish" -> "Sothis" -> {shortName, {id1, id2...}} - std::unordered_map > mTargetToRouteIdMap; - std::unordered_map mRouteIdToNameMap; - - time_t convertBlockIndexToTime(const unsigned int blockIdx); - unsigned int convertTimeToBlockIndex(const time_t& t); - unsigned int getRoutePatternIndex(const unsigned int blockIdx, const unsigned int jump = 0); - std::string createImageNameFromRouteId(const uint32_t& routeId, PRIORITY priority); - std::string createButtonLabelFromRouteId(const uint32_t& routeId, PRIORITY priority); + bool getSecondsUntilNextVoyage( + uint32_t& secondsTillNextVoyage, + uint32_t& secondsLeftInWindow, + const time_t& startTime, + const std::unordered_set& voyageIds, + const std::string& routeNameUsed, + const uint32_t skips = 0 + ); + + std::unordered_set getVoyageIdByTracker( + const std::string& routeName, + const std::string& tracker, + const std::string& name + ); + + void getImageNameAndLabel( + std::string& imageName, + std::string& buttonLabel, + const std::string& routeName, + const std::string& tracker, + const std::string& name, + const PRIORITY priority, + const uint32_t skips + ); + json getTargetsJson(const std::string& routeName); + json getTrackerTypesJson(const std::string& routeName); + json getRouteNames(); - // database loading functions - bool loadSchedule(const json& j); +private: + std::unordered_map> processors; }; \ No newline at end of file diff --git a/Sources/Windows/FFXIVOceanFishingProcessor.cpp b/Sources/Windows/FFXIVOceanFishingProcessor.cpp new file mode 100644 index 0000000..66e4b8b --- /dev/null +++ b/Sources/Windows/FFXIVOceanFishingProcessor.cpp @@ -0,0 +1,748 @@ +//============================================================================== +/** +@file FFXIVOceanFishingProcessor.cpp +@brief Computes Ocean Fishing Times +@copyright (c) 2023, Momoko Tomoko +**/ +//============================================================================== + +#include "pch.h" +#include "FFXIVOceanFishingProcessor.h" +#include +#include +#include +#include +#include "../Vendor/json/src/json.hpp" +using json = nlohmann::json; + +FFXIVOceanFishingProcessor::FFXIVOceanFishingProcessor(const std::string& dataFile) +{ + std::ifstream ifs(dataFile); + + if (ifs.fail()) + { + errorMessage = "Failed to open datafile: " + dataFile; + return; + } + + json j; + bool jsonIsGood = false; + try + { + j = j.parse(ifs); + jsonIsGood = true; + } + catch (...) + { + errorMessage = "Failed to parse dataFile into json object."; + } + ifs.close(); + + if (jsonIsGood) + loadDatabase(j); +} + +FFXIVOceanFishingProcessor::FFXIVOceanFishingProcessor(const json& j) +{ + loadDatabase(j); +} + +bool FFXIVOceanFishingProcessor::loadSchedule(const json& j) +{ + if (isBadKey(j, "name", "Missing route name in database.")) return false; + if (isBadKey(j, "schedule", "Missing schedule in database.")) return false; + if (isBadKey(j["schedule"], "pattern", "Missing pattern in schedule.")) return false; + if (isBadKey(j["schedule"], "offset", "Missing offset in schedule.")) return false; + + // get route name + if (j["name"].is_string()) + mRouteName = j["name"].get(); + else + { + errorMessage = "Invalid route name:\n" + j["schedule"].dump(4); + return false; + } + + // get the pattern and store it in mRoutePattern + for (const auto& id : j["schedule"]["pattern"]) + if (id.is_number_unsigned()) + mRoutePattern.push_back(id.get()); + else + { + errorMessage = "Invalid pattern in schedule: " + id.dump(4) + "\n" + j["schedule"].dump(4); + return false; + } + + // get the offset and store it in mPatternOffset + if (j["schedule"]["offset"].is_number_unsigned()) + mPatternOffset = j["schedule"]["offset"].get(); + else + { + errorMessage = "Invalid offset in schedule:\n" + j["schedule"].dump(4); + return false; + } + return true; +} + +void FFXIVOceanFishingProcessor::loadDatabase(const json& j) +{ + // TODO handle parse errors + // TODO refactor this function + + // get schedule + if (!loadSchedule(j)) return; + + // get stops + for (const auto& stops : j["stops"].get()) + { + mStops.insert({ stops.first, stops.second["shortform"].get() }); + } + + // get fish + if (j["targets"].contains("fish")) + { + for (const auto& fishType : j["targets"]["fish"].get()) + { + mFishes.insert({ fishType.first, {} }); + for (const auto& fish : fishType.second.get()) + { + std::vector locations; + for (const auto& location : fish.second["locations"]) + { + // construct a vector of times the fish is available + // an empty vector means any time is allowed + + std::vector times; + if (location.contains("time")) + { + // location["time"] can be a single entry ("time": "day") or an array ("time": ["day", night"]) + if (location["time"].is_array()) + for (const auto& time : location["time"]) + times.push_back(time.get()); + else + times.push_back(location["time"].get()); + } + + locations.push_back({ location["name"].get(), times }); + } + + std::string shortformName = fish.first; // by default the shortform name is just the fish name + if (fish.second.contains("shortform")) + shortformName = fish.second["shortform"].get(); + + mFishes.at(fishType.first).insert({ fish.first, + {shortformName, + locations} + }); + if (fishType.first == "Blue Fish") + mBlueFishNames.insert({ fish.first, mFishes.at(fishType.first).at(fish.first) }); + } + } + } + + // get achievements + for (const auto& achievement : j["targets"]["achievements"].get()) + { + std::unordered_set ids; + for (const auto routeId : achievement.second["routeIds"]) + { + ids.insert(routeId.get()); + } + mAchievements.insert({achievement.first, ids}); + } + + // get routes + std::unordered_set allRouteIds; + for (const auto& route : j["routes"].get()) + { + std::string routeName = route.first; + if (mRoutes.find(routeName) != mRoutes.end()) + throw std::runtime_error("Error: duplicate route name in json: " + routeName); + if (!route.second["id"].is_number_unsigned()) + throw std::runtime_error("Error: invalid route ID in json: " + route.second["id"].dump(4)); + const uint32_t id = route.second["id"].get(); + if (allRouteIds.contains(id)) + throw std::runtime_error("Error: duplidcate route id in json: " + id); + allRouteIds.insert(id); + + std::vector stops; + for (const auto& stop : route.second["stops"]) + { + const std::string stopName = stop["name"].get(); + const std::string stopTime = stop["time"].get(); + std::unordered_set fishList; + // double check that the stops exist, and create list of fishes + if (mStops.find(stopName) == mStops.end()) + { + throw std::runtime_error("Error: stop " + stopName + " in route " + routeName + " does not exist in j[\"stops\"]"); + } + for (const auto& fishType : mFishes) + { + for (const auto& fish : fishType.second) + { + for (const auto& location : fish.second.locations) + { + if (location.name != stopName) + continue; + + bool isTimeMatch = false; + // empty time vector means any time is allowed + if (location.time.empty()) + isTimeMatch = true; + else + // go through each time and check for any match + for (const auto& time : location.time) + { + if (time == stopTime) + { + isTimeMatch = true; + break; + } + } + + if (!isTimeMatch) + continue; + + fishList.insert(fish.first); + break; + } + } + } + stops.push_back({ {stopName, {stopTime} } , fishList }); + } + + // load achievements into route + std::set achievements; + for (const auto& achievement : mAchievements) + { + if (achievement.second.find(id) != achievement.second.end()) + achievements.insert(achievement.first); + } + + mRoutes.insert({routeName, + { + route.second["shortform"].get(), + id, + stops, + achievements, + "" // bluefishpattern, generated later + } + }); + mRouteIdToNameMap.insert({ id, routeName }); + } + + // construct search target mapping + // targets by blue fish per route + mTargetToRouteIdMap.insert({ "Blue Fish Pattern", {} }); + for (const auto& route : mRoutes) + { + std::string blueFishPattern; + std::unordered_set blueFish; + + // create pattern string as fish1-fish2-fish3, and use X if there is no blue fish + for (const auto& stop : route.second.stops) + { + bool blueFishFound = false; + for (const auto& fish : stop.fish) // go through all the possible fishes at this stop + { + if (mBlueFishNames.find(fish) != mBlueFishNames.end()) // we only care about the blue fish + { + blueFishFound = true; + blueFishPattern += mBlueFishNames.at(fish).shortName; + blueFish.insert(fish); + break; + } + } + if (!blueFishFound) + { + blueFishPattern += "X"; + } + + blueFishPattern += "-"; + } + // remove the last dash + if (blueFishPattern.length() > 0) + blueFishPattern = blueFishPattern.substr(0, blueFishPattern.length() - 1); + + // if only 1 blue fish, use that as the image name without the Xs + std::string imageName = blueFishPattern; + if (blueFish.size() == 1) + imageName = *blueFish.begin(); + + if (blueFishPattern != "X-X-X") + { + mTargetToRouteIdMap.at("Blue Fish Pattern").insert({ blueFishPattern, + { + blueFishPattern, // label name + imageName, // image name + {} // route ids + } + }); + mTargetToRouteIdMap.at("Blue Fish Pattern").at(blueFishPattern).ids.insert(route.second.id); + mRoutes.at(route.first).blueFishPattern = blueFishPattern; + } + } + + // achievements targets: + mTargetToRouteIdMap.insert({ "Achievement", {} }); + for (const auto& achievement : mAchievements) + { + std::unordered_set ids; + for (const auto& routeId : achievement.second) + { + ids.insert(routeId); + } + mTargetToRouteIdMap.at("Achievement").insert({ achievement.first, + { + achievement.first, // achievement label and imagename are the same as just the acheivement name + achievement.first, + ids + } + }); + } + + // fish targets: + for (const auto& fishType : mFishes) + { + mTargetToRouteIdMap.insert({ fishType.first, {} }); + for (const auto& fish : fishType.second) + { + const std::string fishName = fish.first; + std::unordered_set ids; + for (const auto& route : mRoutes) + { + for (const auto& stop : route.second.stops) + { + if (stop.fish.find(fish.first) != stop.fish.end()) + { + ids.insert(route.second.id); + } + } + } + mTargetToRouteIdMap.at(fishType.first).insert({ fishName, + { + fish.first, // fish label and imagename are the same as just the fish name + fish.first, + ids + } + }); + } + } + + // targets by route name: + mTargetToRouteIdMap.insert({ "Routes", {} }); + for (const auto& route : mRoutes) + { + // create an name for this route. Priority goes to achievement, then to the route bluefishpattern + std::string name = route.first; + // TODO: This assumes only 1 achievement per route, although can have more. Right now just grab the first one and use that as the icon + if (route.second.achievements.size() != 0) + name = *route.second.achievements.begin(); + else if (route.second.blueFishPattern.length() > 0) + name = route.second.blueFishPattern; + + std::string lastStop = route.second.stops.back().location.name; + if (mStops.find(lastStop) != mStops.end()) + { + std::string lastStopShortName = mStops.at(lastStop); + + if (mTargetToRouteIdMap.find(lastStopShortName) == mTargetToRouteIdMap.end()) + mTargetToRouteIdMap.at("Routes").insert({ lastStopShortName, {"", "", {}} }); + mTargetToRouteIdMap.at("Routes").at(lastStopShortName).ids.insert(route.second.id); + } + + mTargetToRouteIdMap.at("Routes").insert({ route.first, {name, "", {route.second.id}} }); + } + + // special targets: + mTargetToRouteIdMap.insert({ "Other", {}}); + mTargetToRouteIdMap.at("Other").insert({ "Next Route", {"", "", allRouteIds} }); + + mIsInit = true; +} + +/** + @brief gets the next route number + + @param[out] nextRoute the routeId used + @param[in] startTime the time to start counting from. + @param[in] routeIds A set of routeIds we are looking for. The closest time is returned out of all the routes. = + @param[in] skips number of windows to skip over. Default is 0. + + @return true if successful +**/ +bool FFXIVOceanFishingProcessor::getNextRoute( + uint32_t& nextRoute, + const time_t& startTime, + const std::unordered_set& routeIds, + const uint32_t skips +) +{ + uint32_t relativeSecondsTillNextRoute = 0; + uint32_t relativeWindowTime = 0; + return getSecondsUntilNextRoute(relativeSecondsTillNextRoute, relativeWindowTime, nextRoute, startTime, routeIds, skips); +} + +/** + @brief gets the number of seconds until the next window. If already in a window, it will also get the seconds left in that window + + @param[out] secondsTillNextRoute number of seconds until the next window, not including the one we are currently in + @param[out] secondsLeftInWindow number of seconds left in the current window. Is set to 0 if not in a current window + @param[out] nextRoute the routeId used + @param[in] startTime the time to start counting from. + @param[in] routeIds A set of routeIds we are looking for. The closest time is returned out of all the routes. + @param[in] skips number of windows to skip over. Default is 0. + + @return true if successful +**/ +bool FFXIVOceanFishingProcessor::getSecondsUntilNextRoute( + uint32_t& secondsTillNextRoute, + uint32_t& secondsLeftInWindow, + uint32_t& nextRoute, + const time_t& startTime, + const std::unordered_set & routeIds, + const uint32_t skips +) +{ + bool nextRouteUpdated = false; + secondsTillNextRoute = UINT32_MAX; + secondsLeftInWindow = 0; + + if (routeIds.empty()) + return false; + + // Get the status of where we are currently + unsigned int currBlockIdx = convertTimeToBlockIndex(startTime); + + // Cycle through the route pattern until we get a match to a route we are looking for. + unsigned int skipcounts = 0; + unsigned int maxCycles = 1000; // limit cycles just in case + for (unsigned int i = 0; i < maxCycles; i++) + { + // Current place in the pattern we are looking at. + unsigned int wrappedIdx = getRoutePatternIndex(currBlockIdx, i); + + // Check to see if we match any of our desired routes + // TODO: exit cycle loop if we went through entire pattern with no match, remove maxCycles + if (routeIds.contains(mRoutePattern[wrappedIdx])) + { + // Find the difference in time from the pattern position to the current time + time_t routeTime = convertBlockIndexToTime(currBlockIdx + i); + int timeDifference = static_cast(difftime(routeTime, startTime)); + + // If the time of the route is more than 15m behind us, then it's not a valid route + if (timeDifference < -60 * 15) + { + continue; + } + + // If we are skipping routes, skip now + if (skipcounts < skips) + { + skipcounts++; + continue; + } + + // If we reach here we found a valid route. + // If timeDifference <= 0, that means we are in a window, + // but we still want the time of the next route, so don't return + // and continue for another cycle to get a positive timeDifference + + if (!nextRouteUpdated) // remember the next route. Use the current window as the route if we are in window + { + nextRoute = mRoutePattern[wrappedIdx]; + nextRouteUpdated = true; + } + if (timeDifference <= 0) // needs to have the = also otherwise trigging this on the turn of the hour will not record that we're in a window + { + secondsLeftInWindow = static_cast(60 * 15 + timeDifference); + } + else + { + secondsTillNextRoute = static_cast(timeDifference); + return true; + } + } + } + return false; +} + +/** + @brief gets the route name at a selected time, with option to skip. If in a window, that window is the routes name. If not, the next window will be the name. + + @param[in] t the time to start the check + @param[in] skips number of windows to skip over. Default is 0. + + @return name of the route +**/ +std::string FFXIVOceanFishingProcessor::getNextRouteName(const time_t& t, const unsigned int skips) +{ + unsigned int currBlockIdx = convertTimeToBlockIndex(t); + + unsigned int skipcounts = 0; + const unsigned int maxCycles = 1000; // limit cycles just in case + for (unsigned int i = 0; i < maxCycles; i++) + { + // Find the difference in time from the pattern position to the current time + time_t routeTime = convertBlockIndexToTime(currBlockIdx + i); + int timeDifference = static_cast(difftime(routeTime, t)); + + // If the time of the route is more than 15m behind us, then it's not a valid route + if (timeDifference < -60 * 15) + { + continue; + } + + // If we are skipping routes, skip now + if (skipcounts < skips) + { + skipcounts++; + continue; + } + + // get route index + unsigned int routeIdx = mRoutePattern[getRoutePatternIndex(currBlockIdx + i)]; + + // convert from id + if (mRouteIdToNameMap.find(routeIdx) != mRouteIdToNameMap.end()) + { + const std::string routeName = mRouteIdToNameMap.at(routeIdx); + if (mRoutes.find(routeName) != mRoutes.end()) + return routeName; + } + } + return ""; +} + +/** + @brief converts a block id to a time + this algorithm was obtained from https://github.com/proyebat/FFXIVOceanFishingTimeCalculator + + @param[in] blockIdx the block index to convert + + @return the time in time_t +**/ +time_t FFXIVOceanFishingProcessor::convertBlockIndexToTime(const unsigned int blockIdx) +{ + return static_cast(blockIdx - (mPatternOffset - 1)) * 60 * 60 * 2; +} + +/** + @brief converts a time to a block index + this algorithm was obtained from https://github.com/proyebat/FFXIVOceanFishingTimeCalculator + + @param[in] t the time_t to convert + + @return the block index +**/ +unsigned int FFXIVOceanFishingProcessor::convertTimeToBlockIndex(const time_t& t) +{ + struct tm t_struct {}; + localtime_s(&t_struct, &t); + + // if we are within the 15 boat window, we are still inside the block. Account for this. + if (t_struct.tm_min < 15) + { + t_struct.tm_min -= 15; + } + time_t alignedTime = mktime(&t_struct); + return static_cast(std::ceil(alignedTime / (60 * 60 * 2))) + mPatternOffset; +} + +/** + @brief gets the route's id from the pattern given a block index + + @param[in] blockIdx the block to convert + @param[in] jump number of steps to jump ahead, default is 0 + + @return the route id at the blockIdx + jump +**/ +unsigned int FFXIVOceanFishingProcessor::getRoutePatternIndex(const unsigned int blockIdx, const unsigned int jump) +{ + return (blockIdx + jump) % mRoutePattern.size(); +} + +/** + @brief creates an achievement string name given a voyage name + + @param[in] voyageName the name of the voyage + + @return the achievement name +**/ +std::string FFXIVOceanFishingProcessor::createAchievementName(const std::string & voyageName) +{ + std::string achievementName = ""; + bool firstAchievement = true; + for (const auto& achievement : mRoutes.at(voyageName).achievements) + { + if (!firstAchievement) + achievementName += "-"; + achievementName += achievement; + firstAchievement = false; + } + + return achievementName; +} + +/** + @brief converts a routeId to image name + + @param[in] routeId the routeId to get the image for + @param[in] priority whether to prioritize achievement name or blue fish name + + @return the image name + + @relatesalso getImageName +**/ +std::string FFXIVOceanFishingProcessor::createImageNameFromRouteId(const uint32_t& routeId, PRIORITY priority) +{ + if (mRouteIdToNameMap.find(routeId) == mRouteIdToNameMap.end()) + return ""; + std::string routeName = mRouteIdToNameMap.at(routeId); + if (mRoutes.find(routeName) == mRoutes.end()) + return ""; + + std::string blueFishName = ""; + std::string blueFishPattern = mRoutes.at(routeName).blueFishPattern; + if (mTargetToRouteIdMap.find("Blue Fish Pattern") != mTargetToRouteIdMap.end() && + mTargetToRouteIdMap.at("Blue Fish Pattern").find(blueFishPattern) != mTargetToRouteIdMap.at("Blue Fish Pattern").end()) + blueFishName = mTargetToRouteIdMap.at("Blue Fish Pattern").at(blueFishPattern).imageName; + std::string achievementName = createAchievementName(routeName); + + if ((priority == ACHIEVEMENTS && !achievementName.empty()) || blueFishName.empty()) + return achievementName; + else + return blueFishName; +} + +/** + @brief converts a routeId to button label + + @param[in] routeId the routeId to get the image for + @param[in] priority whether to prioritize achievement name or blue fish name + + @return the button label + + @relatesalso getButtonLabel +**/ +std::string FFXIVOceanFishingProcessor::createButtonLabelFromRouteId(const uint32_t& routeId, PRIORITY priority) +{ + if (mRouteIdToNameMap.find(routeId) == mRouteIdToNameMap.end()) + return ""; + std::string routeName = mRouteIdToNameMap.at(routeId); + if (mRoutes.find(routeName) == mRoutes.end()) + return ""; + + std::string blueFishName = mRoutes.at(routeName).blueFishPattern; + std::string achievementName = createAchievementName(routeName); + + if ((priority == ACHIEVEMENTS && !achievementName.empty()) || blueFishName.empty()) + return achievementName; + else + return blueFishName; +} + +/** + @brief get the image name and label for a particular tracking + + @param[out] imageName the name of the png image for this tracker + @param[out] buttonLabel the string label for this button + @param[in] routeName the name of the route + @param[in] tracker the name of the tracker type (ie: Blue Fish, Achievement) + @param[in] name the name of the actual thing to track (ie: name of fish, name of Achievement) + @param[in] priority whether to prioritize achievement name or blue fish name + @param[in] skips number of windows to skip over +**/ +void FFXIVOceanFishingProcessor::getImageNameAndLabel(std::string& imageName, std::string& buttonLabel, const std::string& tracker, const std::string& name, const PRIORITY priority, const uint32_t skips) +{ + imageName = ""; + buttonLabel = ""; + + // first look for tracker+name in the map + if (mTargetToRouteIdMap.find(tracker) != mTargetToRouteIdMap.end()) + { + if (mTargetToRouteIdMap.at(tracker).find(name) != mTargetToRouteIdMap.at(tracker).end()) + { + buttonLabel = mTargetToRouteIdMap.at(tracker).at(name).labelName; + imageName = mTargetToRouteIdMap.at(tracker).at(name).imageName; + + // if labelName or imageName not provided, we need to create them + if (buttonLabel.empty() || imageName.empty()) + { + // first get routeId for this tracker + std::unordered_set routeIds = getRouteIdByTracker(tracker, name); + + // get next route + time_t startTime = time(0); + uint32_t nextRoute; + if (getNextRoute(nextRoute, startTime, routeIds, skips)) + { + // create the names + if (buttonLabel.empty()) + buttonLabel = createButtonLabelFromRouteId(nextRoute, priority); + if (imageName.empty()) + imageName = createImageNameFromRouteId(nextRoute, priority); + } + } + } + } +} + +/** + @brief gets targets as json + + @return json containing targets +**/ +json FFXIVOceanFishingProcessor::getTargetsJson() +{ + json j; + + for (const auto& type : mTargetToRouteIdMap) + { + for (const auto& target : type.second) + { + j.emplace(target.first, type.first); + } + } + + return j; +} + +/** + @brief gets tracker types as json + + @return json containing tracker types +**/ +json FFXIVOceanFishingProcessor::getTrackerTypesJson() +{ + json j; + + for (const auto& type : mTargetToRouteIdMap) + { + j.push_back(type.first); + } + + std::sort(j.begin(), j.end()); + return j; +} + +/** + @brief converts a target to a set of route ids that matches the target + + @param[in] type the targets type + @param[in] name the targets name + + @return a set of route ids if found, and a null set if the route is not found +**/ +std::unordered_set FFXIVOceanFishingProcessor::getRouteIdByTracker(const std::string& tracker, const std::string& name) +{ + // this map was precomputed in initializer + if (mTargetToRouteIdMap.find(tracker) != mTargetToRouteIdMap.end()) + { + if (mTargetToRouteIdMap.at(tracker).find(name) != mTargetToRouteIdMap.at(tracker).end()) + { + return mTargetToRouteIdMap.at(tracker).at(name).ids; + } + } + return {}; +} \ No newline at end of file diff --git a/Sources/Windows/FFXIVOceanFishingProcessor.h b/Sources/Windows/FFXIVOceanFishingProcessor.h new file mode 100644 index 0000000..60f8670 --- /dev/null +++ b/Sources/Windows/FFXIVOceanFishingProcessor.h @@ -0,0 +1,116 @@ +//============================================================================== +/** +@file FFXIVOceanFishingProcessor.h +@brief Computes Ocean Fishing Times +@copyright (c) 2023, Momoko Tomoko +**/ +//============================================================================== + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "Common.h" + +#include "../Vendor/json/src/json.hpp" +using json = nlohmann::json; + +class FFXIVOceanFishingProcessor +{ +public: + FFXIVOceanFishingProcessor(const std::string& dataFile); + FFXIVOceanFishingProcessor(const json& j); + ~FFXIVOceanFishingProcessor() {}; + + bool isInit() { return mIsInit; }; + std::string getErrorMessage() { return errorMessage; }; + std::string getRouteName() { return mRouteName; }; + + void loadDatabase(const json& j); + + bool getNextRoute(uint32_t& nextRoute, const time_t& startTime, const std::unordered_set& routeIds, const uint32_t skips = 0); + bool getSecondsUntilNextRoute(uint32_t& secondsTillNextRoute, uint32_t& secondsLeftInWindow, uint32_t& nextRoute, const time_t& startTime, const std::unordered_set& routeIds, const uint32_t skips = 0); + std::string getNextRouteName(const time_t& t, const uint32_t skips = 0); + + std::unordered_set getRouteIdByTracker(const std::string& tracker, const std::string& name); + void getImageNameAndLabel(std::string& imageName, std::string& buttonLabel, const std::string& tracker, const std::string& name, const PRIORITY priority, const uint32_t skips); + json getTargetsJson(); + json getTrackerTypesJson(); +private: + bool mIsInit = false; + std::string mRouteName; + std::string errorMessage; + + bool isBadKey(const json& j, const std::string& key, const std::string& msg) + { + if (!j.contains(key)) + { + errorMessage = msg + "\nJson Dump:\n" + j.dump(4); + return true; + } + return false; + } + + struct locations_t + { + const std::string name; + const std::vector time; + }; + + struct fish_t + { + const std::string shortName; + const std::vector locations; + }; + + + std::unordered_map mStops; + std::unordered_map> mFishes; + std::unordered_map> mAchievements; + std::map mBlueFishNames; + + struct stop_t + { + const locations_t location; + const std::unordered_set fish; + }; + + struct route_t + { + const std::string shortName; + const uint32_t id; + const std::vector stops; + const std::set achievements; + std::string blueFishPattern; + }; + std::unordered_map mRoutes; + + uint32_t mPatternOffset = 0; + std::vector mRoutePattern; + + struct targets_t + { + const std::string labelName; + const std::string imageName; + std::unordered_set ids; + }; + // hiearchy is target type -> target name -> struct with vector of route ids + // ie: "Blue Fish" -> "Sothis" -> {shortName, {id1, id2...}} + std::unordered_map > mTargetToRouteIdMap; + std::unordered_map mRouteIdToNameMap; + + time_t convertBlockIndexToTime(const unsigned int blockIdx); + unsigned int convertTimeToBlockIndex(const time_t& t); + unsigned int getRoutePatternIndex(const unsigned int blockIdx, const unsigned int jump = 0); + + std::string createAchievementName(const std::string& voyageName); + std::string createImageNameFromRouteId(const uint32_t& routeId, PRIORITY priority); + std::string createButtonLabelFromRouteId(const uint32_t& routeId, PRIORITY priority); + + // database loading functions + bool loadSchedule(const json& j); +}; \ No newline at end of file diff --git a/Sources/Windows/PluginTests/FFXIVOceanFishingHelperInitializationTests.cpp b/Sources/Windows/PluginTests/FFXIVOceanFishingHelperInitializationTests.cpp deleted file mode 100644 index 41b9b41..0000000 --- a/Sources/Windows/PluginTests/FFXIVOceanFishingHelperInitializationTests.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "pch.h" - -#include -#include -#include -#include "../FFXIVOceanFishingHelper.h" - -namespace { - using json = nlohmann::json; - - const std::string dataFile = "../../com.elgato.ffxivoceanfishing.sdPlugin/oceanFishingDatabase.json"; - TEST(FFXIVOceanFishingHelperInitializationTests, ValidJson) { - std::ifstream ifs(dataFile); - if (ifs.fail()) { - FAIL() << "Cannot open: " << dataFile; - } - json j = j.parse(ifs); - ifs.close(); - } - - TEST(FFXIVOceanFishingHelperInitializationTests, DatabaseInitialization) { - FFXIVOceanFishingHelper mFFXIVOceanFishingHelper(dataFile); - std::cout << mFFXIVOceanFishingHelper.getErrorMessage() << std::endl; - ASSERT_TRUE(mFFXIVOceanFishingHelper.isInit()); - } - - TEST(FFXIVOceanFishingHelperInitializationTests, DatabaseNotFound) { - FFXIVOceanFishingHelper mFFXIVOceanFishingHelper(std::string("./NonExistantFile")); - std::cout << mFFXIVOceanFishingHelper.getErrorMessage() << std::endl; - ASSERT_FALSE(mFFXIVOceanFishingHelper.isInit()); - } - - - class FFXIVOceanFishingHelperInitDatabaseTestFixture : - public ::testing::TestWithParam<::testing::tuple> { - }; - - INSTANTIATE_TEST_CASE_P( - FFXIVOceanFishingHelperLoadDataBaseTests, - FFXIVOceanFishingHelperInitDatabaseTestFixture, - ::testing::Combine( - ::testing::Values("", R"("pattern": [0.5])", R"("pattern": ["b"])"), - ::testing::Values("", R"("offset": 0.5)", R"("offset": "a")") - )); - - TEST_P(FFXIVOceanFishingHelperInitDatabaseTestFixture, MalformedJson) { - std::string pattern = std::get<0>(GetParam()); - std::string offset = std::get<1>(GetParam()); - std::string comma = (pattern.length() > 0 && offset.length() > 0) ? "," : ""; - std::string j_string = R"({"schedule": {)" + pattern + comma + offset + "}}"; - json j = json::parse(j_string); - FFXIVOceanFishingHelper mFFXIVOceanFishingHelper(j); - std::cout << mFFXIVOceanFishingHelper.getErrorMessage() << std::endl; - ASSERT_FALSE(mFFXIVOceanFishingHelper.isInit()); - } -} \ No newline at end of file diff --git a/Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests .cpp b/Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests .cpp new file mode 100644 index 0000000..55f15ed --- /dev/null +++ b/Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests .cpp @@ -0,0 +1,275 @@ +//copyright (c) 2023, Momoko Tomoko + +#include "pch.h" + +#include +#include +#include +#include "../FFXIVOceanFishingHelper.h" +#include "../TimeUtils.hpp" + +namespace { + using json = nlohmann::json; + + class FFXIVOceanFishingHelperBase + { + protected: + const std::vector dataFiles = + { + "../../com.elgato.ffxivoceanfishing.sdPlugin/oceanFishingDatabase - Indigo Route.json", + "../../com.elgato.ffxivoceanfishing.sdPlugin/oceanFishingDatabase - Ruby Route.json" + }; + + std::unique_ptr< FFXIVOceanFishingHelper> mFFXIVOceanFishingHelper; + }; + + class FFXIVOceanFishingHelperTests : + public FFXIVOceanFishingHelperBase, + public ::testing::Test + { + protected: + void SetUp() + { + mFFXIVOceanFishingHelper.reset(new FFXIVOceanFishingHelper(dataFiles)); + } + }; + + TEST_F(FFXIVOceanFishingHelperTests, DatabaseInitialization) { + std::cout << mFFXIVOceanFishingHelper->getErrorMessage() << std::endl; + ASSERT_TRUE(mFFXIVOceanFishingHelper->isInit()); + + json jRouteNames = mFFXIVOceanFishingHelper->getRouteNames(); + std::cout << jRouteNames.dump() << std::endl; + + ASSERT_EQ(jRouteNames, json::parse(R"( +[ + "Indigo Route", + "Ruby Route" +] + )") + ); + } + + TEST_F(FFXIVOceanFishingHelperTests, GetTargetsJsonIndigo) { + json jTargets = mFFXIVOceanFishingHelper->getTargetsJson("Indigo Route"); + std::cout << jTargets.dump(4) << std::endl; + + // green fish test + EXPECT_EQ(jTargets["Aetheric Seadragon"].get(), "Green Fish"); + + // blue fish test + EXPECT_EQ(jTargets["Hafgufa"].get(), "Blue Fish"); + + // achievement + EXPECT_EQ(jTargets["Balloon"].get(), "Achievement"); + + // routes + EXPECT_EQ(jTargets["Bloodbrine"].get(), "Routes"); + EXPECT_EQ(jTargets["Bloodbrine by Day"].get(), "Routes"); + + // pattern + EXPECT_EQ(jTargets["Soth-X-Stone"].get(), "Blue Fish Pattern"); + } + + TEST_F(FFXIVOceanFishingHelperTests, GetTargetsJsonRuby) { + json jTargets = mFFXIVOceanFishingHelper->getTargetsJson("Ruby Route"); + std::cout << jTargets.dump(4) << std::endl; + + // green fish test + EXPECT_EQ(jTargets["Fishy Shark"].get(), "Green Fish"); + + // blue fish test + EXPECT_EQ(jTargets["Hells' Claw"].get(), "Blue Fish"); + + // achievement + EXPECT_EQ(jTargets["Shrimp"].get(), "Achievement"); + + // routes + EXPECT_EQ(jTargets["One River"].get(), "Routes"); + EXPECT_EQ(jTargets["One River by Day"].get(), "Routes"); + + // pattern + EXPECT_EQ(jTargets["X-Glass-Jewel"].get(), "Blue Fish Pattern"); + } + TEST_F(FFXIVOceanFishingHelperTests, GetTrackerTypesJson) { + json jTrackerTypes = mFFXIVOceanFishingHelper->getTrackerTypesJson("Indigo Route"); + std::cout << jTrackerTypes.dump(4) << std::endl; + + EXPECT_EQ(jTrackerTypes, json::parse(R"( +[ + "Achievement", + "Any Next Route", + "Blue Fish", + "Blue Fish Pattern", + "Green Fish", + "Routes" +] + )") + ); + } + + class FFXIVOceanFishingHelperNoRouteFixture : + public FFXIVOceanFishingHelperBase, + public ::testing::TestWithParam< + ::testing::tuple< + uint32_t, + uint32_t, + time_t, + std::unordered_set, + std::string, + uint32_t>> + { + protected: + void SetUp() + { + mFFXIVOceanFishingHelper.reset(new FFXIVOceanFishingHelper(dataFiles)); + } + }; + + std::unordered_set zeroId = { 0 }; + std::unordered_set emptyId = { }; + + INSTANTIATE_TEST_CASE_P( + NoRouteTests, + FFXIVOceanFishingHelperNoRouteFixture, + ::testing::Combine( + ::testing::Values(UINT_MAX), // seconds till next route + ::testing::Values(0), // window time + ::testing::Values(0, 1), // start time + ::testing::Values( // routes + zeroId, emptyId + ), + ::testing::Values("Indigo Route", "Ruby Route", "Invalid Name"), // routeName + ::testing::Values(0, 1, 2) // skips + ) + ); + + TEST_P(FFXIVOceanFishingHelperNoRouteFixture, NoNextRoute) { + uint32_t relativeSecondsTillNextRoute; + uint32_t relativeWindowTime; + std::string routeName; + + ASSERT_FALSE(mFFXIVOceanFishingHelper->getSecondsUntilNextVoyage( + relativeSecondsTillNextRoute, + relativeWindowTime, + std::get<2>(GetParam()), + std::get<3>(GetParam()), + std::get<4>(GetParam()), + std::get<5>(GetParam()) + )); + + EXPECT_EQ(std::get<0>(GetParam()), relativeSecondsTillNextRoute); + EXPECT_EQ(std::get<1>(GetParam()), relativeWindowTime); + } + + class FFXIVOceanFishingHelperNextRouteFixture : + public FFXIVOceanFishingHelperBase, + public ::testing::TestWithParam< + ::testing::tuple< + uint32_t, + uint32_t, + time_t, + std::tuple>, + uint32_t>> + { + protected: + void SetUp() + { + mFFXIVOceanFishingHelper.reset(new FFXIVOceanFishingHelper(dataFiles)); + } + }; + + std::tuple> indigo1 = { "Indigo Route", { 9 } }; + std::tuple> ruby1 = { "Ruby Route", { 3 } }; + + INSTANTIATE_TEST_CASE_P( + GetFirstRouteSeconds, + FFXIVOceanFishingHelperNextRouteFixture, + ::testing::Combine( + ::testing::Values(7200), // seconds till next route + ::testing::Values(0), // window time + ::testing::Values(0, 1, 7199), // start time + ::testing::Values( // routes + indigo1, + ruby1 + ), + ::testing::Values(0) // skips + ) + ); + + TEST_P(FFXIVOceanFishingHelperNextRouteFixture, getSecondsUntilNextVoyage) { + uint32_t relativeSecondsTillNextRoute = 0; + uint32_t relativeWindowTime = 0; + + ASSERT_TRUE(mFFXIVOceanFishingHelper->getSecondsUntilNextVoyage( + relativeSecondsTillNextRoute, + relativeWindowTime, + std::get<2>(GetParam()), + std::get<1>(std::get<3>(GetParam())), + std::get<0>(std::get<3>(GetParam())), + std::get<4>(GetParam()) + )); + + std::cout << "Next Route Time: " << timeutils::convertSecondsToHMSString(relativeSecondsTillNextRoute) << std::endl; + std::cout << "Window Time: " << timeutils::convertSecondsToHMSString(relativeWindowTime) << std::endl; + + EXPECT_EQ(std::get<0>(GetParam())-std::get<2>(GetParam()), relativeSecondsTillNextRoute); + EXPECT_EQ(std::get<1>(GetParam()), relativeWindowTime); + } + + class FFXIVOceanFishingHelperSkipRouteFixture : + public FFXIVOceanFishingHelperBase, + public ::testing::TestWithParam< + ::testing::tuple< + time_t, + std::tuple>, + uint32_t>> + { + protected: + void SetUp() + { + mFFXIVOceanFishingHelper.reset(new FFXIVOceanFishingHelper(dataFiles)); + } + }; + + INSTANTIATE_TEST_CASE_P( + FFXIVOceanFishingHelperRouteProcessingTests, + FFXIVOceanFishingHelperSkipRouteFixture, + ::testing::Combine( + ::testing::Values(0, 60000), // start time + ::testing::Values( // routes + indigo1, + ruby1 + ), + ::testing::Values(1,2,3) // skips + ) + ); + + TEST_P(FFXIVOceanFishingHelperSkipRouteFixture, GetSecondsUntilNextVoyage) { + // first get the expected time until the window + skips + uint32_t relativeSecondsTillNextRoute0 = 0; + uint32_t relativeWindowTime0 = 0; + ASSERT_TRUE(mFFXIVOceanFishingHelper->getSecondsUntilNextVoyage( + relativeSecondsTillNextRoute0, + relativeWindowTime0, + std::get<0>(GetParam()), + std::get<1>(std::get<1>(GetParam())), + std::get<0>(std::get<1>(GetParam())), + std::get<2>(GetParam()) + )); + + // go to the time predicted above -1, the predicted time should be 1 second + uint32_t relativeSecondsTillNextRoute1 = 0; + uint32_t relativeWindowTime1 = 0; + std::string routeName1; + ASSERT_TRUE(mFFXIVOceanFishingHelper->getSecondsUntilNextVoyage( + relativeSecondsTillNextRoute1, + relativeWindowTime1, + relativeSecondsTillNextRoute0 + std::get<0>(GetParam()) - 1, + std::get<1>(std::get<1>(GetParam())), + std::get<0>(std::get<1>(GetParam())), + 0 + )); + EXPECT_EQ(1, relativeSecondsTillNextRoute1); + } +} \ No newline at end of file diff --git a/Sources/Windows/PluginTests/FFXIVOceanFishingProcessorInitializationTests.cpp b/Sources/Windows/PluginTests/FFXIVOceanFishingProcessorInitializationTests.cpp new file mode 100644 index 0000000..7e4563d --- /dev/null +++ b/Sources/Windows/PluginTests/FFXIVOceanFishingProcessorInitializationTests.cpp @@ -0,0 +1,58 @@ +//copyright (c) 2023, Momoko Tomoko + +#include "pch.h" + +#include +#include +#include +#include "../FFXIVOceanFishingProcessor.h" + +namespace { + using json = nlohmann::json; + + const std::string dataFile = "../../com.elgato.ffxivoceanfishing.sdPlugin/oceanFishingDatabase - Indigo Route.json"; + TEST(FFXIVOceanFishingProcessorInitializationTests, ValidJson) { + std::ifstream ifs(dataFile); + if (ifs.fail()) { + FAIL() << "Cannot open: " << dataFile; + } + json j = j.parse(ifs); + ifs.close(); + } + + TEST(FFXIVOceanFishingProcessorInitializationTests, DatabaseInitialization) { + FFXIVOceanFishingProcessor mFFXIVOceanFishingProcessor(dataFile); + std::cout << mFFXIVOceanFishingProcessor.getErrorMessage() << std::endl; + ASSERT_TRUE(mFFXIVOceanFishingProcessor.isInit()); + } + + TEST(FFXIVOceanFishingProcessorInitializationTests, DatabaseNotFound) { + FFXIVOceanFishingProcessor mFFXIVOceanFishingProcessor(std::string("./NonExistantFile")); + std::cout << mFFXIVOceanFishingProcessor.getErrorMessage() << std::endl; + ASSERT_FALSE(mFFXIVOceanFishingProcessor.isInit()); + } + + + class FFXIVOceanFishingProcessorInitDatabaseTestFixture : + public ::testing::TestWithParam<::testing::tuple> { + }; + + INSTANTIATE_TEST_CASE_P( + FFXIVOceanFishingProcessorLoadDataBaseTests, + FFXIVOceanFishingProcessorInitDatabaseTestFixture, + ::testing::Combine( + ::testing::Values("", R"("pattern": [0.5])", R"("pattern": ["b"])"), + ::testing::Values("", R"("offset": 0.5)", R"("offset": "a")") + )); + + TEST_P(FFXIVOceanFishingProcessorInitDatabaseTestFixture, MalformedJson) { + std::string pattern = std::get<0>(GetParam()); + std::string offset = std::get<1>(GetParam()); + std::string comma = (pattern.length() > 0 && offset.length() > 0) ? "," : ""; + std::string j_string = R"({"schedule": {)" + pattern + comma + offset + "}}"; + json j = json::parse(j_string); + FFXIVOceanFishingProcessor mFFXIVOceanFishingProcessor(j); + std::cout << mFFXIVOceanFishingProcessor.getErrorMessage() << std::endl; + ASSERT_FALSE(mFFXIVOceanFishingProcessor.isInit()); + } +} \ No newline at end of file diff --git a/Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests.cpp b/Sources/Windows/PluginTests/FFXIVOceanFishingProcessorTests.cpp similarity index 83% rename from Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests.cpp rename to Sources/Windows/PluginTests/FFXIVOceanFishingProcessorTests.cpp index 2035c42..9533c55 100644 --- a/Sources/Windows/PluginTests/FFXIVOceanFishingHelperTests.cpp +++ b/Sources/Windows/PluginTests/FFXIVOceanFishingProcessorTests.cpp @@ -1,6 +1,8 @@ +//copyright (c) 2023, Momoko Tomoko + #include "pch.h" -#include "../FFXIVOceanFishingHelper.h" +#include "../FFXIVOceanFishingProcessor.h" namespace { @@ -12,16 +14,17 @@ namespace PRIORITY priority; } createButtonLabelFromRouteIdTestParams; - class FFXIVOceanFishingHelperTests : public ::testing::TestWithParam + class FFXIVOceanFishingProcessorTests : public ::testing::TestWithParam { protected: void SetUp() { - mFFXIVOceanFishingHelper.reset(new FFXIVOceanFishingHelper(j)); + mFFXIVOceanFishingProcessor.reset(new FFXIVOceanFishingProcessor(j)); } json j = json::parse(R"( { + "name": "Test Route", "stops": { "StopA": { "shortform": "A" @@ -136,20 +139,21 @@ namespace } } )"); - std::unique_ptr< FFXIVOceanFishingHelper> mFFXIVOceanFishingHelper; + + std::unique_ptr mFFXIVOceanFishingProcessor; }; - TEST_F(FFXIVOceanFishingHelperTests, getRouteIdByTrackerAchievementTest) { + TEST_F(FFXIVOceanFishingProcessorTests, getRouteIdByTrackerAchievementTest) { std::string tracker = "Achievement"; std::string name = "AchieveAB"; - std::unordered_set ids = mFFXIVOceanFishingHelper->getRouteIdByTracker(tracker, name); + std::unordered_set ids = mFFXIVOceanFishingProcessor->getRouteIdByTracker(tracker, name); ASSERT_EQ(ids, std::unordered_set({ 1,2 })); } - TEST_F(FFXIVOceanFishingHelperTests, getRouteIdByTrackerTest) { + TEST_F(FFXIVOceanFishingProcessorTests, getRouteIdByTrackerTest) { std::string tracker = "Blue Fish"; std::string name = "Fish from A night or sunset"; - std::unordered_set ids = mFFXIVOceanFishingHelper->getRouteIdByTracker(tracker, name); + std::unordered_set ids = mFFXIVOceanFishingProcessor->getRouteIdByTracker(tracker, name); ASSERT_EQ(ids, std::unordered_set({ 1,2 })); } } \ No newline at end of file diff --git a/Sources/Windows/PluginTests/PluginTests.vcxproj b/Sources/Windows/PluginTests/PluginTests.vcxproj index 61d3069..4abc4aa 100644 --- a/Sources/Windows/PluginTests/PluginTests.vcxproj +++ b/Sources/Windows/PluginTests/PluginTests.vcxproj @@ -34,12 +34,16 @@ + + - - + + + + Create Create @@ -98,6 +102,7 @@ MultiThreadedDLL Level3 ProgramDatabase + stdcpp20 true diff --git a/Sources/Windows/com.elgato.ffxivoceanfishing.sdPlugin.vcxproj b/Sources/Windows/com.elgato.ffxivoceanfishing.sdPlugin.vcxproj index f3b4e7e..d32968f 100644 --- a/Sources/Windows/com.elgato.ffxivoceanfishing.sdPlugin.vcxproj +++ b/Sources/Windows/com.elgato.ffxivoceanfishing.sdPlugin.vcxproj @@ -198,6 +198,7 @@ + @@ -234,6 +235,7 @@ /FI"pch.h" %(AdditionalOptions) + Create Create @@ -244,7 +246,8 @@ - + + diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Balloon-Manta.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Balloon-Manta.png new file mode 100644 index 0000000..b89aa2a Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Balloon-Manta.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Bekko Rockhugger.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Bekko Rockhugger.png new file mode 100644 index 0000000..7c69382 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Bekko Rockhugger.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Black-jawed Helicoprion.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Black-jawed Helicoprion.png new file mode 100644 index 0000000..25adb7f Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Black-jawed Helicoprion.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Crimson Sentry.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Crimson Sentry.png new file mode 100644 index 0000000..d6c49c0 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Crimson Sentry.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Dusk Shark.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Dusk Shark.png new file mode 100644 index 0000000..5f2ac18 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Dusk Shark.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Fishy Shark.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Fishy Shark.png new file mode 100644 index 0000000..92b87f1 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Fishy Shark.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Gakugyo.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Gakugyo.png new file mode 100644 index 0000000..2fa4a19 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Gakugyo.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Ginrin Goshiki.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Ginrin Goshiki.png new file mode 100644 index 0000000..ce24d2b Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Ginrin Goshiki.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Glass Dragon.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Glass Dragon.png new file mode 100644 index 0000000..81b290f Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Glass Dragon.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Heavensent Shark.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Heavensent Shark.png new file mode 100644 index 0000000..39ff409 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Heavensent Shark.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Hells' Claw.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Hells' Claw.png new file mode 100644 index 0000000..fff7f8a Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Hells' Claw.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Iridescent Trout.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Iridescent Trout.png new file mode 100644 index 0000000..de46431 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Iridescent Trout.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Jewel of Plum Spring.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Jewel of Plum Spring.png new file mode 100644 index 0000000..5f03804 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Jewel of Plum Spring.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mailfish.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mailfish.png new file mode 100644 index 0000000..5b5d278 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mailfish.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mizuhiki.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mizuhiki.png new file mode 100644 index 0000000..bdcb6c7 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Mizuhiki.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Pitch Pickle.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Pitch Pickle.png new file mode 100644 index 0000000..afab71d Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Pitch Pickle.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish-Shrimp.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish-Shrimp.png new file mode 100644 index 0000000..b4722f5 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish-Shrimp.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish.png new file mode 100644 index 0000000..36077b7 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shellfish.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shrimp.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shrimp.png new file mode 100644 index 0000000..37a7df2 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Shrimp.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Spadefish.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Spadefish.png new file mode 100644 index 0000000..81e0944 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Spadefish.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Squid.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Squid.png new file mode 100644 index 0000000..0b53cda Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Squid.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Stingfin Trevally.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Stingfin Trevally.png new file mode 100644 index 0000000..e3c1a3a Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Stingfin Trevally.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Taniwha.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Taniwha.png new file mode 100644 index 0000000..77c6c67 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Taniwha.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Un-Namazu.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Un-Namazu.png new file mode 100644 index 0000000..d07b68d Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Un-Namazu.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Vivid Pink Shrimp.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Vivid Pink Shrimp.png new file mode 100644 index 0000000..bc0f394 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Vivid Pink Shrimp.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/X-Glass-Jewel.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/X-Glass-Jewel.png new file mode 100644 index 0000000..4775a08 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/X-Glass-Jewel.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Yellow Iris.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Yellow Iris.png new file mode 100644 index 0000000..fca61dc Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/Yellow Iris.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/default.png b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/default.png new file mode 100644 index 0000000..38accf5 Binary files /dev/null and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/Icons/default.png differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing.exe b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing.exe index a539db0..61926d6 100644 Binary files a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing.exe and b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing.exe differ diff --git a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing_pi.html b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing_pi.html index cdbfd7f..10d3da9 100644 --- a/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing_pi.html +++ b/Sources/com.elgato.ffxivoceanfishing.sdPlugin/ffxivoceanfishing_pi.html @@ -6,6 +6,12 @@ +
+
Route
+ +
Track by