diff --git a/README.md b/README.md index 450c33a..86b0005 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ An open source project providing [Rich Presence](https://discord.com/rich-presence) for [PlanetSide 2](https://www.planetside2.com/home). - + + -![Build Workflow Status](https://img.shields.io/github/workflow/status/leonhard-s/ps2-rich-presence/build) ![License](https://img.shields.io/github/license/leonhard-s/ps2-rich-presence) ![Total Downloads](https://img.shields.io/github/downloads/leonhard-s/ps2-rich-presence/total) -![GitHub Release Date](https://img.shields.io/github/release-date/leonhard-s/ps2-rich-presence) +![GitHub Release Date](https://img.shields.io/github/release-date/leonhard-s/ps2-rich-presence?label=latest%20version) [Overview](#overview) • [Features](#features) • [Limitations](#limitations) • [License](#license) • [Installation](#installation) • [Contributing](#contributing) @@ -23,21 +23,24 @@ This application runs independently of the main PlanetSide 2 executable and does ## Work In Progress -This utility is still in its early stages of development. Many features are missing (see below) and bugs are certain. If you find some, please do not hesitate to [report them](https://github.com/leonhard-s/ps2-rich-presence/issues). +This utility is still in its early stages of development. Many features are missing (see below) and bugs are next to certain. + +If you find some, please do not hesitate to [report them](https://github.com/leonhard-s/ps2-rich-presence/issues). ### Noteworthy omissions +These are planned features that are in development but not yet available in the current version: + - No start-on-login support - No minimizing to tray - No automatic tracking of characters on login or failover when they log off ## Features -- **Display your character's status on Discord** -- **Detection of current continent, class, and vehicle** -- **Lightweight** -- **Low bandwidth requirements** -- ~~**Automatic tracking upon login**~~ (soon) +- Display your character's status on Discord +- Detection of current continent, class, and vehicle +- Lightweight and low bandwidth usage +- ~~Automatic tracking upon login~~ (soon) ## Limitations @@ -71,10 +74,49 @@ You are free to use and modify these screenshots for your own projects given com ## Installation -This project is still under development and must be installed from source. A standalone installer and portable ZIP version will be provided for all releases. +You can find an installer package for the latest version of PS2RPC in the project [Releases](https://github.com/leonhard-s/ps2-rich-presence/releases). + +### Building from Source + +The following is a non-exhaustive guide to building this project from source. + +1. This application is based on Qt and requires it to be built. You can download Qt from [qt.io](https://www.qt.io/download-open-source/), or via your operating system's package manager. + + The Qt version required is v6.2.0 or later. + + Additionally, you will need the [Qt WebSockets](https://github.com/qt/qtwebsockets) extension. When using the online installer, you can select it from the "Additional Libraries" section. Refer to your local search engine for information when installing Qt another way. + +2. The project is fully configured through CMake. Note that you may have to configure CMake itself to tell it where Qt has been installed to - adding the Qt installation to `CMAKE_PREFIX_PATH` is a painless way to do so. + +3. Clone this repository using Git: + + ```bash + git clone https://github.com/leonhard-s/ps2-rich-presence + ``` + + Set your current working directory to the root of the cloned project: + + ```bash + cd ps2-rich-presence + ``` + +4. Run CMake to configure the project: + + ```bash + cmake . -B build + ``` + +5. Build the project: + + ```bash + cmake --build build --target install + ``` + + The `--target install` suffix will automatically install the application into the `build/local_install` subdirectory, including any required dependencies. ## Contributing If you encounter any issues using PS2RPC or would like to suggest a new feature or change, feel free to get in touch via the repository [issues](https://github.com/leonhard-s/ps2-rich-presence/issues). -Please also consider lack of clarity or undocumented features when creating issues. Using this application should be straightforward for anyone familiar with the game, and any improvement to make the app clearer and more intuitive to use is worth discussing. +Please also consider lack of clarity or undocumented features when creating issues. +Using this application should be straightforward for anyone familiar with the game, and any improvement to make the app clearer and more intuitive to use is worth discussing. diff --git a/assets/README.md b/assets/README.md index 87d71c7..55ebaa7 100644 --- a/assets/README.md +++ b/assets/README.md @@ -40,3 +40,20 @@ Processing notes: | small_image/sunderer.png | 77942 | [Link](https://census.daybreakgames.com/files/ps2/images/static/77942.png) | 1 | | small_image/valkyrie.png | 79813 | [Link](https://census.daybreakgames.com/files/ps2/images/static/79813.png) | 1 | | small_image/vanguard.png | 77943 | [Link](https://census.daybreakgames.com/files/ps2/images/static/77943.png) | 1 | + +## In-Game Screenshots + +The following assets were created from in-game screenshots taken by project contributors. + +Processing notes: + +**1:** Cropped and graded for easy distinction at small sizes. + +| Asset | Source Image | Processing | +|---------------------------|---------------------------|------------| +| large_image/amerish.png | screenshots/amerish.png | 1 | +| large_image/esamir.png | screenshots/esamir.png | 1 | +| large_image/hossin.png | screenshots/hossin.png | 1 | +| large_image/indar.png | screenshots/indar.png | 1 | +| large_image/oshur.png | screenshots/oshur.png | 1 | +| large_image/sanctuary.png | screenshots/sanctuary.png | 1 | diff --git a/assets/large_image/amerish.png b/assets/large_image/amerish.png new file mode 100644 index 0000000..a939b37 Binary files /dev/null and b/assets/large_image/amerish.png differ diff --git a/assets/large_image/esamir.png b/assets/large_image/esamir.png new file mode 100644 index 0000000..26bc767 Binary files /dev/null and b/assets/large_image/esamir.png differ diff --git a/assets/large_image/hossin.png b/assets/large_image/hossin.png new file mode 100644 index 0000000..7e02f55 Binary files /dev/null and b/assets/large_image/hossin.png differ diff --git a/assets/large_image/indar.png b/assets/large_image/indar.png new file mode 100644 index 0000000..9bb6173 Binary files /dev/null and b/assets/large_image/indar.png differ diff --git a/assets/large_image/oshur.png b/assets/large_image/oshur.png new file mode 100644 index 0000000..746899f Binary files /dev/null and b/assets/large_image/oshur.png differ diff --git a/assets/large_image/sanctuary.png b/assets/large_image/sanctuary.png new file mode 100644 index 0000000..5daae12 Binary files /dev/null and b/assets/large_image/sanctuary.png differ diff --git a/assets/screenshots/amerish.png b/assets/screenshots/amerish.png new file mode 100644 index 0000000..e716426 Binary files /dev/null and b/assets/screenshots/amerish.png differ diff --git a/assets/screenshots/esamir.png b/assets/screenshots/esamir.png new file mode 100644 index 0000000..5c744d0 Binary files /dev/null and b/assets/screenshots/esamir.png differ diff --git a/assets/screenshots/hossin.png b/assets/screenshots/hossin.png new file mode 100644 index 0000000..8618197 Binary files /dev/null and b/assets/screenshots/hossin.png differ diff --git a/assets/screenshots/indar.png b/assets/screenshots/indar.png new file mode 100644 index 0000000..435424b Binary files /dev/null and b/assets/screenshots/indar.png differ diff --git a/assets/screenshots/oshur.png b/assets/screenshots/oshur.png new file mode 100644 index 0000000..a472570 Binary files /dev/null and b/assets/screenshots/oshur.png differ diff --git a/assets/screenshots/sanctuary.png b/assets/screenshots/sanctuary.png new file mode 100644 index 0000000..6df3109 Binary files /dev/null and b/assets/screenshots/sanctuary.png differ diff --git a/cmake/QtInstallDebug.cmake b/cmake/QtInstallDebug.cmake index d9c9a46..5687e5d 100644 --- a/cmake/QtInstallDebug.cmake +++ b/cmake/QtInstallDebug.cmake @@ -2,7 +2,7 @@ # "install(SCRIPT ...)" command does not provide an easy way of passing # arguments or build configuration info to its script target. -set(project_root "${CMAKE_CURRENT_SOURCE_DIR}/../.") +set(project_root "${CMAKE_CURRENT_LIST_DIR}/../.") set(target_exe_name "Ps2 Rich Presence") set(windeployqt_temp "${project_root}/build/windeployqt_temp") @@ -16,7 +16,7 @@ endif() set(target_exe_path "${project_root}/build/Debug/${target_exe_name}.exe") if (NOT EXISTS "${target_exe_path}") - message(WARNING "Could not locate built binary, windeployqt not run") + message(STATUS "Could not locate debug binary, windeployqt not run") return() endif() diff --git a/cmake/QtInstallRelease.cmake b/cmake/QtInstallRelease.cmake index cdf76ae..f914d44 100644 --- a/cmake/QtInstallRelease.cmake +++ b/cmake/QtInstallRelease.cmake @@ -2,7 +2,7 @@ # "install(SCRIPT ...)" command does not provide an easy way of passing # arguments or build configuration info to its script target. -set(project_root "${CMAKE_CURRENT_SOURCE_DIR}/../.") +set(project_root "${CMAKE_CURRENT_LIST_DIR}/../.") set(target_exe_name "Ps2 Rich Presence") set(windeployqt_temp "${project_root}/build/windeployqt_temp") @@ -16,7 +16,7 @@ endif() set(target_exe_path "${project_root}/build/Release/${target_exe_name}.exe") if (NOT EXISTS "${target_exe_path}") - message(WARNING "Could not locate built binary, windeployqt not run") + message(STATUS "Could not locate release binary, windeployqt not run") return() endif() diff --git a/ps2rpc/appdata/assets.cpp b/ps2rpc/appdata/assets.cpp index 07ec729..2ccecf8 100644 --- a/ps2rpc/appdata/assets.cpp +++ b/ps2rpc/appdata/assets.cpp @@ -117,22 +117,22 @@ namespace assets switch (zone) { case ps2::Zone::Indar: - image_key = "zone_indar"; + image_key = "indar"; break; case ps2::Zone::Hossin: - image_key = "zone_hossin"; + image_key = "hossin"; break; case ps2::Zone::Amerish: - image_key = "zone_amerish"; + image_key = "amerish"; break; case ps2::Zone::Esamir: - image_key = "zone_esamir"; + image_key = "esamir"; break; case ps2::Zone::Oshur: - image_key = "zone_oshur"; + image_key = "oshur"; break; case ps2::Zone::Sanctuary: - image_key = "zone_sanctuary"; + image_key = "sanctuary"; break; default: return -1; diff --git a/ps2rpc/game/state.cpp b/ps2rpc/game/state.cpp index b86b7e5..443c251 100644 --- a/ps2rpc/game/state.cpp +++ b/ps2rpc/game/state.cpp @@ -30,7 +30,7 @@ namespace ps2rpc } GameStateFactory::GameStateFactory(arx::character_id_t character_id, ps2::Faction faction, ps2::Server server, ps2::Class class_) - : character_id_{character_id}, faction_{faction}, server_{server}, class_{class_}, vehicle_{ps2::Vehicle::None}, team_{ps2::Faction::NS}, zone_{ps2::Zone::Sanctuary} {} + : character_id_{character_id}, faction_{faction}, server_{server}, class_{class_}, vehicle_{ps2::Vehicle::None}, team_{faction}, zone_{ps2::Zone::Sanctuary} {} arx::character_id_t GameStateFactory::getCharacterId() const noexcept { diff --git a/ps2rpc/tracker.cpp b/ps2rpc/tracker.cpp index 28531ba..06a4e66 100644 --- a/ps2rpc/tracker.cpp +++ b/ps2rpc/tracker.cpp @@ -2,8 +2,7 @@ #include "tracker.hpp" -#include -#include +#include #include #include #include @@ -40,9 +39,15 @@ namespace ps2rpc : QObject(parent), character_{character}, state_factory_{character.id, character.faction, character.server, character.class_}, current_state_{}, ess_client_{} { + // Set initial state via state factory + state_factory_.buildState(current_state_); // Create WebSocket client for event streaming endpoint ess_client_.reset(new EssClient(SERVICE_ID, this)); - ess_client_->subscribe(generateSubscription()); + auto subs = generateSubscriptions(); + for (auto subscription : subs) + { + ess_client_->subscribe(subscription); + } ess_client_->connect(); QObject::connect(ess_client_.get(), &EssClient::payloadReceived, this, &ActivityTracker::onPayloadReceived); @@ -57,8 +62,36 @@ namespace ps2rpc const arx::json_t &payload) { emit payloadReceived(event_name, payload); + // Update state factory based on payload + if (event_name == "Death") + { + handleDeathPayload(payload); + } + else if (event_name == "GainExperience") + { + handleGainexperiencePayload(payload); + } + else + { + qDebug() << "Ignoring payload for unhandled event:" << event_name; + return; + } + // Check if the new state is different from the old state + GameState state; + if (state_factory_.buildState(state)) + { + qWarning() << "Unable to build state from factory"; + return; + } + if (state != current_state_) + { + current_state_ = state; + emit stateChanged(state); + } + } - // Character ID + void ActivityTracker::handleDeathPayload(const arx::json_t &payload) + { bool are_we_the_baddies = integerFromApiString(payload["attacker_character_id"]) == character_.id; // Team // TODO: Implement team ID once it is implemented on the API side @@ -66,7 +99,7 @@ namespace ps2rpc // Class arx::loadout_id_t loadout_id = are_we_the_baddies ? integerFromApiString(payload["attacker_loadout_id"]) : integerFromApiString(payload["character_loadout_id"]); - ps2::Class class_; + ps2::Class class_ = state_factory_.getProfileAsClass(); if (ps2::class_from_loadout_id(loadout_id, class_)) { qWarning() << "Unable to get class from loadout ID:" << loadout_id; @@ -88,7 +121,7 @@ namespace ps2rpc ps2::vehicle_from_vehicle_id(vehicle_id, vehicle); // Zone arx::zone_id_t zone_id = integerFromApiString(payload["zone_id"]); - ps2::Zone zone; + ps2::Zone zone = state_factory_.getZone(); if (ps2::zone_from_zone_id(zone_id, zone)) { qWarning() << "Unable to get zone from zone ID:" << zone_id; @@ -104,28 +137,53 @@ namespace ps2rpc } state_factory_.setTeam(team); state_factory_.setZone(zone); - // Check if the new state is different from the old state - GameState state; - if (state_factory_.buildState(state)) + } + + void ActivityTracker::handleGainexperiencePayload(const arx::json_t &payload) + { + // Class + arx::loadout_id_t loadout_id = integerFromApiString(payload["loadout_id"]); + ps2::Class class_; + if (ps2::class_from_loadout_id(loadout_id, class_)) { - qWarning() << "Unable to build state from factory"; - return; + qWarning() << "Unable to get class from loadout ID:" << loadout_id; } - if (state != current_state_) + + // TODO: We could use the experience type itself to make further + // guesses about the character's activity, such as gunner or pilot + // assists. + + // Zone + arx::zone_id_t zone_id = integerFromApiString(payload["zone_id"]); + ps2::Zone zone = state_factory_.getZone(); + if (ps2::zone_from_zone_id(zone_id, zone)) { - current_state_ = state; - emit stateChanged(state); + qWarning() << "Unable to get zone from zone ID:" << zone_id; } + // Update state factory + if (state_factory_.getProfileAsVehicle() == ps2::Vehicle::None) + { + // We only override the profile with the current class if we're + // already tracking as an infantry class, since we cannot tell if + // the player is still in a vehicle from the experience tick alone. + state_factory_.setProfile(class_); + } + state_factory_.setZone(zone); } - arx::Subscription ActivityTracker::generateSubscription() const + QList ActivityTracker::generateSubscriptions() const { - auto sub = arx::Subscription( + auto deaths = arx::Subscription( // Event names {"Death"}, // Characters {QString::number(character_.id).toStdString()}); - return sub; + auto experience = arx::Subscription( + // Event names + {"GainExperience"}, + // Characters + {QString::number(character_.id).toStdString()}); + return QList{deaths, experience}; } } // namespace ps2rpc diff --git a/ps2rpc/tracker.hpp b/ps2rpc/tracker.hpp index d83cd91..7ef4806 100644 --- a/ps2rpc/tracker.hpp +++ b/ps2rpc/tracker.hpp @@ -3,7 +3,7 @@ #ifndef PS2RPC_TRACKER_HPP #define PS2RPC_TRACKER_HPP -#include +#include #include #include #include @@ -39,7 +39,9 @@ namespace ps2rpc const arx::json_t &payload); private: - arx::Subscription generateSubscription() const; + QList generateSubscriptions() const; + void handleDeathPayload(const arx::json_t &payload); + void handleGainexperiencePayload(const arx::json_t &payload); CharacterData character_; GameStateFactory state_factory_;