mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-29 03:46:50 +01:00
Launcher: the privacy policy and terms of service, and an "I agree" before playing
Both documents come from the ReSkate backend (GET /api/v1/legal/privacy and /terms, edited on its Legal page), so a change reaches players without a release. Settings has a button for each, opening a panel like the changelogs': title, version and date, the Markdown drawn as the README viewer draws it, and a switch to the other one. Each launch looks up the current versions (GET /api/v1/legal) in the background. One newer than the player agreed to, or none agreed to yet, puts up an agreement over the home screen: a short summary, a way into each document, QUIT and I AGREE, which saves the versions agreed to (accepted_privacy_version, accepted_terms_version in the launcher's settings). A backend that cannot be reached holds nobody up. Launches that skip the window (--no-gui, Discord joins) are not asked.
This commit is contained in:
1 parent
d15e51e558
commit
4f1f5a882e
6 files changed
+260
-2
No files matched your search
@@ -407,6 +407,8 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
// MODS tile can say when installed mods have updates.
|
||||
collect_listing(mods_panel);
|
||||
refresh_listing(mods_panel, time);
|
||||
// Whether the privacy policy or terms changed since the player agreed (looked up once).
|
||||
check_legal_versions(ui, launcher.settings());
|
||||
// A mod dropped on the window opens the Mods panel and installs it.
|
||||
{
|
||||
std::lock_guard lock(g_dropped_mutex);
|
||||
@@ -429,7 +431,7 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
ui.steam_offline = !launcher.settings().offline && !launcher_app::steam_signed_in();
|
||||
}
|
||||
}
|
||||
const bool modal = ui.settings || ui.changelog || ui.mods || ui.sign_in || qr_open || state.prompt.has_value() ||
|
||||
const bool modal = ui.settings || ui.changelog || ui.legal.open >= 0 || ui.legal.agreement || ui.mods || ui.sign_in || qr_open || state.prompt.has_value() ||
|
||||
ui.steam_offline || state.phase == Phase::mods_broken || ui.mods_update_prompt;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
@@ -563,6 +565,9 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
if (state.prompt) prompt_window(launcher, fonts, size, *state.prompt, ui);
|
||||
else if (qr_open) qr_window(launcher, fonts, size, state.qr);
|
||||
else if (ui.sign_in) sign_in_window(launcher, fonts, size, ui);
|
||||
// Opened from Settings or the agreement, and back to it when closed.
|
||||
else if (ui.legal.open >= 0) legal_window(fonts, size, ui);
|
||||
else if (ui.legal.agreement) legal_agreement_window(launcher, fonts, size, ui, window);
|
||||
else if (ui.settings) settings_window(launcher, fonts, size, ui, window);
|
||||
else if (ui.changelog) {
|
||||
ui.notes.prereleases = launcher.settings().prereleases;
|
||||
|
||||
@@ -210,6 +210,8 @@ struct Settings {
|
||||
std::string steam_username; // never the password
|
||||
bool steam_remember{true};
|
||||
bool steam_prefer_code{};
|
||||
// The versions of the privacy policy and the terms of service the player agreed to (0: none).
|
||||
std::uint64_t accepted_privacy{}, accepted_terms{};
|
||||
};
|
||||
|
||||
inline constexpr std::array<const char*, 7> log_levels{"trace", "debug", "info", "warning", "error", "critical", "off"};
|
||||
@@ -348,6 +350,45 @@ struct Changelog {
|
||||
}
|
||||
};
|
||||
|
||||
// The privacy policy and terms of service, from the ReSkate backend (GET /api/v1/legal/<kind>):
|
||||
// each fetched on a worker the first time it is shown and kept while the launcher runs.
|
||||
// Everything but the worker's hand-over belongs to the UI thread.
|
||||
struct Legal {
|
||||
enum Kind { privacy, terms, count };
|
||||
struct Document {
|
||||
std::string title, updated, markdown; // updated: YYYY-MM-DD
|
||||
std::uint64_t version{};
|
||||
};
|
||||
struct Slot {
|
||||
std::thread worker;
|
||||
std::atomic<bool> loading{};
|
||||
bool arrived{}; // worker -> UI, under the mutex
|
||||
Document incoming;
|
||||
std::string incoming_error;
|
||||
|
||||
bool loaded{};
|
||||
std::string error; // why the last fetch failed
|
||||
Document document;
|
||||
thunderstore::Readme text;
|
||||
};
|
||||
std::mutex mutex;
|
||||
std::array<Slot, count> slots;
|
||||
int open{-1}; // the Kind shown, or -1 when the window is closed
|
||||
|
||||
// The versions the backend has now (GET /api/v1/legal), looked up once a launch. One newer
|
||||
// than the player agreed to (Settings::accepted_*) puts up the agreement before anything else.
|
||||
std::thread versions_worker;
|
||||
bool versions_started{}, versions_arrived{}; // arrived: worker -> UI, under the mutex
|
||||
std::array<std::uint64_t, count> current{};
|
||||
bool agreement{}; // the agreement window is up
|
||||
|
||||
~Legal() {
|
||||
for (auto& slot : slots)
|
||||
if (slot.worker.joinable()) slot.worker.join();
|
||||
if (versions_worker.joinable()) versions_worker.join();
|
||||
}
|
||||
};
|
||||
|
||||
// Settings > GRAPHICS, AUDIO, CAMERA, CONTROLS, REPLAY: skate.'s own settings, read from its
|
||||
// save each time Settings opens (game_settings.h).
|
||||
struct GamePages {
|
||||
@@ -364,6 +405,7 @@ struct Ui {
|
||||
GamePages game;
|
||||
bool changelog{};
|
||||
Changelog notes;
|
||||
Legal legal;
|
||||
int settings_tab{}; // GAME, DISPLAY, KEYS, ADVANCED, then the game's own pages
|
||||
int binding{}; // 1 = menu key, 2 = console key, while waiting for a press
|
||||
std::string key_error;
|
||||
@@ -614,6 +656,13 @@ void qr_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, const std::v
|
||||
void steam_offline_window(const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
// What the newest release changed: its notes on GitHub.
|
||||
void changelog_window(const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
// The privacy policy or the terms of service (ui.legal.open), with a switch between them.
|
||||
void legal_window(const Fonts& fonts, ImVec2 size, Ui& ui);
|
||||
// Each frame: the backend's versions of both, looked up once; a newer one than the player
|
||||
// agreed to raises ui.legal.agreement.
|
||||
void check_legal_versions(Ui& ui, const Settings& settings);
|
||||
// "I agree" to the current versions, or quit.
|
||||
void legal_agreement_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, HWND window);
|
||||
void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, HWND window);
|
||||
void mods_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, ModsPanel& panel, HWND window);
|
||||
// Shown instead of launching when the merge left mods out.
|
||||
|
||||
@@ -71,6 +71,8 @@ Settings load_settings(const fs::path& path) {
|
||||
settings.steam_username = json.value("steam_username", std::string());
|
||||
settings.steam_remember = json.value("steam_remember", settings.steam_remember);
|
||||
settings.steam_prefer_code = json.value("steam_prefer_code", settings.steam_prefer_code);
|
||||
settings.accepted_privacy = json.value("accepted_privacy_version", settings.accepted_privacy);
|
||||
settings.accepted_terms = json.value("accepted_terms_version", settings.accepted_terms);
|
||||
} catch (const std::exception& exception) {
|
||||
logging::log(logging::Level::warning, logging::Channel::launcher, "Ignoring launcher settings: {}", exception.what());
|
||||
}
|
||||
@@ -98,6 +100,8 @@ void save_settings(const fs::path& path, const Settings& settings) {
|
||||
json["steam_username"] = settings.steam_username;
|
||||
json["steam_remember"] = settings.steam_remember;
|
||||
json["steam_prefer_code"] = settings.steam_prefer_code;
|
||||
json["accepted_privacy_version"] = settings.accepted_privacy;
|
||||
json["accepted_terms_version"] = settings.accepted_terms;
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
output << json.dump(2) << '\n';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "gui_internal.h"
|
||||
|
||||
#include "text_encoding.h"
|
||||
|
||||
#include "Engine/Core/Json/json.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
|
||||
// The LEGAL panel: the privacy policy and terms of service as the ReSkate backend has them now
|
||||
// (GET https://api.reskate.dev/api/v1/legal/<kind>), so a change reaches players without a release.
|
||||
namespace dingosdk::launcher_gui::detail {
|
||||
namespace {
|
||||
constexpr const char* kinds[Legal::count]{"privacy", "terms"};
|
||||
constexpr const char* names[Legal::count]{"PRIVACY POLICY", "TERMS OF SERVICE"};
|
||||
constexpr std::uint64_t answer_limit = 256 * 1024;
|
||||
constexpr int timeout_ms = 15000;
|
||||
|
||||
void start_fetch(Legal& legal, int kind) {
|
||||
auto& slot = legal.slots[static_cast<std::size_t>(kind)];
|
||||
if (slot.loading.exchange(true)) return;
|
||||
if (slot.worker.joinable()) slot.worker.join();
|
||||
slot.error.clear();
|
||||
slot.worker = std::thread([&legal, &slot, kind] {
|
||||
Legal::Document document;
|
||||
std::string error;
|
||||
try {
|
||||
std::string body;
|
||||
const auto url = std::string("https://api.reskate.dev/api/v1/legal/") + kinds[kind];
|
||||
launcher_update::http_stream(launcher_text::wide(url), answer_limit, timeout_ms,
|
||||
[&](const char* data, DWORD size) { body.append(data, size); });
|
||||
const auto root = Json::parse(body);
|
||||
document.title = root.at("title").string();
|
||||
document.markdown = root.at("markdown").string();
|
||||
document.version = root.at("version").get<std::uint64_t>();
|
||||
document.updated = root.at("updated_at").string().substr(0, 10);
|
||||
if (document.markdown.empty()) throw std::runtime_error("the document came back empty");
|
||||
} catch (const std::exception& failure) {
|
||||
error = failure.what();
|
||||
logging::write(logging::Level::warning, logging::Channel::launcher,
|
||||
std::format("Could not get the {}: {}", kinds[kind], error));
|
||||
}
|
||||
std::lock_guard lock(legal.mutex);
|
||||
slot.incoming = std::move(document);
|
||||
slot.incoming_error = std::move(error);
|
||||
slot.arrived = true;
|
||||
slot.loading = false;
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void legal_window(const Fonts& fonts, ImVec2 size, Ui& ui) {
|
||||
auto& legal = ui.legal;
|
||||
const int kind = legal.open;
|
||||
auto& slot = legal.slots[static_cast<std::size_t>(kind)];
|
||||
{
|
||||
std::lock_guard lock(legal.mutex);
|
||||
for (auto& each : legal.slots)
|
||||
if (each.arrived) {
|
||||
each.arrived = false;
|
||||
each.error = std::move(each.incoming_error);
|
||||
if (each.error.empty()) {
|
||||
each.document = std::move(each.incoming);
|
||||
each.text = thunderstore::readme_lines(each.document.markdown, 2000);
|
||||
each.loaded = true;
|
||||
}
|
||||
each.incoming = {};
|
||||
}
|
||||
}
|
||||
if (!slot.loaded && !slot.loading && slot.error.empty()) start_fetch(legal, kind);
|
||||
|
||||
const auto panel = begin_panel("##legal_panel", size, ImVec2(S(880), S(680)));
|
||||
panel_title(fonts, names[kind]);
|
||||
const float footer = ImGui::GetFrameHeight() + S(24) + S(12);
|
||||
ImGui::BeginChild("##legal_body", ImVec2(0, panel.y - ImGui::GetCursorPosY() - footer), ImGuiChildFlags_NavFlattened);
|
||||
if (slot.loading && !slot.loaded) {
|
||||
ImGui::TextDisabled("Loading...");
|
||||
} else if (!slot.loaded) {
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextDisabled("Could not load it: %s", slot.error.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::Button("Try again", ImVec2(S(120), 0))) start_fetch(legal, kind);
|
||||
} else {
|
||||
const auto& document = slot.document;
|
||||
ImGui::PushFont(fonts.heading);
|
||||
ImGui::TextUnformatted(document.title.c_str());
|
||||
ImGui::PopFont();
|
||||
ImGui::TextDisabled("Version %llu, updated %s", static_cast<unsigned long long>(document.version), document.updated.c_str());
|
||||
ImGui::Dummy(ImVec2(0, S(6)));
|
||||
draw_readme(fonts, slot.text);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SetCursorPosY(panel.y - S(24) - ImGui::GetFrameHeight());
|
||||
const int other = kind == Legal::privacy ? Legal::terms : Legal::privacy;
|
||||
if (ImGui::Button(kind == Legal::privacy ? "Terms of Service" : "Privacy Policy", ImVec2(S(180), 0))) legal.open = other;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(panel.x - S(28) - S(110));
|
||||
const bool close = ImGui::Button("CLOSE", ImVec2(S(110), 0));
|
||||
default_focus();
|
||||
if (close || ImGui::IsKeyPressed(ImGuiKey_Escape, false)) legal.open = -1;
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void check_legal_versions(Ui& ui, const Settings& settings) {
|
||||
auto& legal = ui.legal;
|
||||
if (!legal.versions_started) {
|
||||
legal.versions_started = true;
|
||||
legal.versions_worker = std::thread([&legal] {
|
||||
std::array<std::uint64_t, Legal::count> found{};
|
||||
try {
|
||||
std::string body;
|
||||
launcher_update::http_stream(L"https://api.reskate.dev/api/v1/legal", answer_limit, timeout_ms,
|
||||
[&](const char* data, DWORD size) { body.append(data, size); });
|
||||
const auto root = Json::parse(body);
|
||||
const auto& documents = root.at("documents");
|
||||
for (std::size_t index = 0; index < documents.size(); ++index) {
|
||||
const auto& document = documents.at(index);
|
||||
const auto kind = document.at("kind").string();
|
||||
for (int at = 0; at < Legal::count; ++at)
|
||||
if (kind == kinds[at]) found[static_cast<std::size_t>(at)] = document.at("version").get<std::uint64_t>();
|
||||
}
|
||||
} catch (const std::exception& failure) {
|
||||
// Nothing to show: the launcher does not hold anyone up over it.
|
||||
logging::write(logging::Level::warning, logging::Channel::launcher,
|
||||
std::format("Could not check the privacy policy and terms: {}", failure.what()));
|
||||
}
|
||||
std::lock_guard lock(legal.mutex);
|
||||
legal.current = found;
|
||||
legal.versions_arrived = true;
|
||||
});
|
||||
}
|
||||
std::lock_guard lock(legal.mutex);
|
||||
if (!legal.versions_arrived) return;
|
||||
legal.versions_arrived = false;
|
||||
const auto& current = legal.current;
|
||||
legal.agreement = current[Legal::privacy] > settings.accepted_privacy || current[Legal::terms] > settings.accepted_terms;
|
||||
if (legal.agreement)
|
||||
logging::write(logging::Level::info, logging::Channel::launcher,
|
||||
std::format("Privacy policy {} and terms {} to agree to (agreed: {} and {}).", current[Legal::privacy],
|
||||
current[Legal::terms], settings.accepted_privacy, settings.accepted_terms));
|
||||
}
|
||||
|
||||
void legal_agreement_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, HWND window) {
|
||||
auto& legal = ui.legal;
|
||||
auto& settings = launcher.settings();
|
||||
const bool first = !settings.accepted_privacy && !settings.accepted_terms;
|
||||
const auto panel = begin_panel("##legal_agreement", size, ImVec2(S(640), S(470)));
|
||||
panel_title(fonts, first ? "BEFORE YOU PLAY" : "UPDATED TERMS");
|
||||
ImGui::PushTextWrapPos(0);
|
||||
if (first) {
|
||||
ImGui::TextUnformatted("ReSkate has terms of service and a privacy policy. Read them, and agree to keep using ReSkate.");
|
||||
} else {
|
||||
std::string changed;
|
||||
if (legal.current[Legal::terms] > settings.accepted_terms) changed = "terms of service";
|
||||
if (legal.current[Legal::privacy] > settings.accepted_privacy)
|
||||
changed += changed.empty() ? "privacy policy" : " and the privacy policy";
|
||||
ImGui::TextUnformatted(std::format("The {} changed since you last agreed. Read what changed, and agree to keep using "
|
||||
"ReSkate.", changed).c_str());
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, S(8)));
|
||||
ImGui::TextDisabled("In short:");
|
||||
for (const char* line : {"Play fair online and respect other players.",
|
||||
"Don't unlock or copy cosmetics skate. sells.",
|
||||
"Crash reports go to the ReSkate team unless you turn them off in Settings. Nothing in them is "
|
||||
"meant to identify you.",
|
||||
"ReSkate is a free fan project, not made by EA, and comes with no warranty."})
|
||||
ImGui::BulletText("%s", line);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, S(10)));
|
||||
if (ImGui::Button("Read the Terms of Service", ImVec2(S(260), 0))) legal.open = Legal::terms;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Read the Privacy Policy", ImVec2(S(260), 0))) legal.open = Legal::privacy;
|
||||
|
||||
ImGui::SetCursorPosY(panel.y - S(24) - ImGui::GetFrameHeight());
|
||||
if (ImGui::Button("QUIT", ImVec2(S(110), 0))) PostMessageW(window, WM_CLOSE, 0, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(panel.x - S(28) - S(160));
|
||||
push_primary_button();
|
||||
const bool agree = ImGui::Button("I AGREE", ImVec2(S(160), 0));
|
||||
pop_primary_button();
|
||||
default_focus();
|
||||
if (agree) {
|
||||
settings.accepted_privacy = (std::max)(settings.accepted_privacy, legal.current[Legal::privacy]);
|
||||
settings.accepted_terms = (std::max)(settings.accepted_terms, legal.current[Legal::terms]);
|
||||
launcher.save();
|
||||
legal.agreement = false;
|
||||
logging::write(logging::Level::info, logging::Channel::launcher,
|
||||
std::format("Agreed to privacy policy {} and terms {}.", settings.accepted_privacy, settings.accepted_terms));
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace dingosdk::launcher_gui::detail
|
||||
@@ -473,6 +473,11 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui
|
||||
"about your account is included. Needs crash reports on; the launcher picks a change up when it "
|
||||
"next starts.",
|
||||
settings.performance_timings);
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Privacy Policy")) ui.legal.open = Legal::privacy;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Terms of Service")) ui.legal.open = Legal::terms;
|
||||
setting_note("What ReSkate collects and sends, and the rules for using it and its servers.");
|
||||
break;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ if(WIN32)
|
||||
add_executable(dingosdk_launcher WIN32 Launcher/main.cpp Launcher/launch.cpp
|
||||
Launcher/gui.cpp Launcher/gui_launcher.cpp Launcher/gui_renderer.cpp Launcher/gui_home.cpp
|
||||
Launcher/gui_settings.cpp Launcher/gui_sign_in.cpp Launcher/gui_mods.cpp Launcher/gui_mods_browse.cpp Launcher/gui_mods_profiles.cpp
|
||||
Launcher/gui_gamepad.cpp Launcher/gamepad_input.cpp Launcher/gui_changelog.cpp Launcher/game_settings.cpp
|
||||
Launcher/gui_gamepad.cpp Launcher/gamepad_input.cpp Launcher/gui_changelog.cpp Launcher/gui_legal.cpp Launcher/game_settings.cpp
|
||||
Launcher/updater.cpp Launcher/mod_manager.cpp Launcher/mod_profiles.cpp Launcher/thunderstore.cpp Launcher/official_mods.cpp
|
||||
Launcher/problem.h)
|
||||
target_link_libraries(dingosdk_launcher PRIVATE dingosdk_logging dingosdk_content_cache_install dingosdk_world_layer_scan dingosdk_launcher_support dingosdk_initfs
|
||||
|
||||
Reference in new issue
Block a user