mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 09:45:53 +02:00
Show a missing map's zip size on the join prompt before the download starts
This commit is contained in:
8 files changed
+90
-1
No files matched your search
@@ -3,8 +3,10 @@
|
||||
#include <winhttp.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
@@ -189,6 +191,74 @@ Download get(std::wstring_view url, const std::filesystem::path& destination,
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> content_length(std::wstring_view url, std::uint32_t timeout_seconds,
|
||||
const wchar_t* agent, Watch* watch) {
|
||||
const auto started = now_us();
|
||||
const auto timeout_deadline = started + std::uint64_t{timeout_seconds} * 1000000;
|
||||
const auto stop = [&]() -> DWORD {
|
||||
if (watch && watch->cancel.load()) return ERROR_CANCELLED;
|
||||
const auto external = watch ? watch->deadline_us.load() : 0;
|
||||
const auto deadline = external && external < timeout_deadline ? external : timeout_deadline;
|
||||
return now_us() >= deadline ? ERROR_TIMEOUT : ERROR_SUCCESS;
|
||||
};
|
||||
if (stop() || url.empty() || url.size() >= 4096 || url.find_first_of(L"\r\n\t #") != std::wstring_view::npos)
|
||||
return std::nullopt;
|
||||
const std::wstring address(url);
|
||||
URL_COMPONENTS parts{}; parts.dwStructSize = sizeof(parts);
|
||||
parts.dwHostNameLength = parts.dwUrlPathLength = parts.dwExtraInfoLength =
|
||||
parts.dwUserNameLength = parts.dwPasswordLength = static_cast<DWORD>(-1);
|
||||
if (!WinHttpCrackUrl(address.c_str(), static_cast<DWORD>(address.size()), 0, &parts) ||
|
||||
parts.nScheme != INTERNET_SCHEME_HTTPS || !parts.dwHostNameLength || parts.dwUserNameLength || parts.dwPasswordLength)
|
||||
return std::nullopt;
|
||||
const std::wstring host(parts.lpszHostName, parts.dwHostNameLength);
|
||||
std::wstring resource = parts.dwUrlPathLength ? std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) : L"/";
|
||||
if (parts.dwExtraInfoLength) resource.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
|
||||
Internet session{WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, WINHTTP_FLAG_ASYNC)};
|
||||
if (!session.value || !WinHttpSetTimeouts(session.value, 5000, 5000, 10000, 10000)) return std::nullopt;
|
||||
Internet connection{WinHttpConnect(session.value, host.c_str(), parts.nPort, 0)};
|
||||
if (!connection.value) return std::nullopt;
|
||||
Internet request{WinHttpOpenRequest(connection.value, L"HEAD", resource.c_str(), nullptr, nullptr,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)};
|
||||
if (!request.value) return std::nullopt;
|
||||
AsyncRequest async;
|
||||
DWORD redirect = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
|
||||
DWORD logon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
|
||||
if (!WinHttpSetOption(request.value, WINHTTP_OPTION_REDIRECT_POLICY, &redirect, sizeof(redirect)) ||
|
||||
!WinHttpSetOption(request.value, WINHTTP_OPTION_AUTOLOGON_POLICY, &logon, sizeof(logon)))
|
||||
return std::nullopt;
|
||||
DWORD_PTR context = reinterpret_cast<DWORD_PTR>(&async);
|
||||
if (!WinHttpSetOption(request.value, WINHTTP_OPTION_CONTEXT_VALUE, &context, sizeof(context))) return std::nullopt;
|
||||
if (WinHttpSetStatusCallback(request.value, AsyncRequest::callback,
|
||||
WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE | WINHTTP_CALLBACK_FLAG_HEADERS_AVAILABLE |
|
||||
WINHTTP_CALLBACK_FLAG_REQUEST_ERROR | WINHTTP_CALLBACK_FLAG_HANDLES, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
|
||||
return std::nullopt;
|
||||
async.value = std::exchange(request.value, nullptr);
|
||||
const auto finish = [&](BOOL accepted) -> DWORD {
|
||||
if (!accepted && GetLastError() != ERROR_IO_PENDING) return GetLastError();
|
||||
return async.wait(stop);
|
||||
};
|
||||
if (stop()) return std::nullopt;
|
||||
async.prepare(WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE);
|
||||
if (finish(WinHttpSendRequest(async.value, nullptr, 0, nullptr, 0, 0, context))) return std::nullopt;
|
||||
async.prepare(WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE);
|
||||
if (stop() || finish(WinHttpReceiveResponse(async.value, nullptr))) return std::nullopt;
|
||||
DWORD status{}, size = sizeof(status);
|
||||
if (!WinHttpQueryHeaders(async.value, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
nullptr, &status, &size, nullptr) || status != 200)
|
||||
return std::nullopt;
|
||||
// As text: a number query is 32 bits, and a map can be larger than that.
|
||||
wchar_t digits[32]{};
|
||||
size = sizeof(digits);
|
||||
if (!WinHttpQueryHeaders(async.value, WINHTTP_QUERY_CONTENT_LENGTH, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
digits, &size, WINHTTP_NO_HEADER_INDEX) || !digits[0])
|
||||
return std::nullopt;
|
||||
wchar_t* end{};
|
||||
errno = 0;
|
||||
const auto length = std::wcstoull(digits, &end, 10);
|
||||
if (end == digits || *end || errno == ERANGE || !length) return std::nullopt;
|
||||
return length;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_text(std::wstring_view url, std::uint64_t max_bytes,
|
||||
std::uint32_t timeout_seconds, const wchar_t* agent, Download* result, Watch* watch) {
|
||||
static std::atomic<unsigned> serial{};
|
||||
|
||||
@@ -28,4 +28,8 @@ Download get(std::wstring_view url, const std::filesystem::path& destination,
|
||||
// fails, and `result`, when given, says how.
|
||||
std::optional<std::string> get_text(std::wstring_view url, std::uint64_t max_bytes,
|
||||
std::uint32_t timeout_seconds, const wchar_t* agent, Download* result = nullptr, Watch* watch = nullptr);
|
||||
// Content-Length of an HTTPS HEAD, when the server gives one. The body is not read. Nothing
|
||||
// when the request fails or the length is absent: a caller can go on without a size.
|
||||
std::optional<std::uint64_t> content_length(std::wstring_view url, std::uint32_t timeout_seconds,
|
||||
const wchar_t* agent, Watch* watch = nullptr);
|
||||
}
|
||||
@@ -78,6 +78,9 @@ DWORD test_get_temp_path(DWORD capacity, LPWSTR output) {
|
||||
#undef now_us
|
||||
|
||||
namespace dingosdk::https {
|
||||
std::optional<std::uint64_t> content_length(std::wstring_view, std::uint32_t, const wchar_t*, Watch*) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<std::string> get_text(std::wstring_view, std::uint64_t, std::uint32_t,
|
||||
const wchar_t*, Download* result, Watch* watch) {
|
||||
fixture::require(watch && watch->deadline_us.load() == map_download::deadline.load(),
|
||||
|
||||
@@ -46,6 +46,7 @@ package::Name wanted; // under mutex
|
||||
std::string version; // under mutex: the one to download
|
||||
std::string newest; // under mutex
|
||||
std::string description; // under mutex: Thunderstore's, or the installed mod's own
|
||||
std::uint64_t package_bytes{}; // under mutex: the zip's Content-Length
|
||||
bool installed_off{}; // under mutex: here already, switched off
|
||||
std::string last_failed; // under mutex: a package not to offer again this run
|
||||
https::Watch watch;
|
||||
@@ -228,11 +229,15 @@ void check() {
|
||||
// Another version is only worth fetching when it is the host's: any other leaves the two apart still.
|
||||
if (update && choice.version != multiplayer::map_package_version(need_package()))
|
||||
return fail("That version is not on Thunderstore any more. Ask the " + host_word(need_server()) + " to update the map, or install its version yourself.");
|
||||
// The package page names the version and not its size. The download's Content-Length is
|
||||
// what the card shows; the body is not fetched until the player says yes.
|
||||
const auto length = https::content_length(package::download_url(name, choice.version), 8, agent, &watch);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
version = choice.version;
|
||||
newest = choice.newest;
|
||||
description = choice.description;
|
||||
package_bytes = length.value_or(0);
|
||||
}
|
||||
// Its icon for the card: small, and not worth holding the question up for long.
|
||||
if (!choice.icon.empty())
|
||||
@@ -407,6 +412,7 @@ void offer(const multiplayer::MapNeed& asked) {
|
||||
version.clear();
|
||||
newest.clear();
|
||||
description.clear();
|
||||
package_bytes = 0;
|
||||
installed_off = false;
|
||||
}
|
||||
overlay::set_map_download_icon({});
|
||||
@@ -471,6 +477,7 @@ View view() {
|
||||
out.author = wanted.owner;
|
||||
out.version = version;
|
||||
out.description = description;
|
||||
out.bytes = package_bytes;
|
||||
if (out.stage == Stage::applying) {
|
||||
const auto now = live_mods::progress();
|
||||
out.step = now.done;
|
||||
|
||||
@@ -42,6 +42,7 @@ struct View {
|
||||
bool installed{}; // asking: the mod is here but switched off, so nothing is downloaded
|
||||
std::string have; // the version installed here, when the host's is another and replaces it
|
||||
bool choice{true}; // asking: the answer a controller has picked (true: download)
|
||||
std::uint64_t bytes{}; // asking: the zip's Content-Length; 0 when the server did not say
|
||||
std::uint64_t received{}, total{}; // downloading; total 0 when the size is not known
|
||||
std::uint32_t yes_bind{}, no_bind{}; // the vote binds, which answer the card
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// The card for a map a multiplayer session needs and this PC does not have
|
||||
// (Extension/Assets/map_download.h), laid out as the mod's page is: its icon, name, author,
|
||||
// version and description, then what is being asked or done. It asks whether to download it,
|
||||
// version, size and description, then what is being asked or done. It asks whether to download it,
|
||||
// in the middle of the screen with the controls under it as the game's own menus have them.
|
||||
// Once the player has said yes it becomes a small card at the right edge, with the download,
|
||||
// the install and the apply, so they can skate on while they wait; it goes when the map is in
|
||||
@@ -223,6 +223,8 @@ void draw_asked_map(State& s) {
|
||||
std::vector<Fact> facts{{"Author", card.author}};
|
||||
if (!card.version.empty()) facts.push_back({"Version", card.version});
|
||||
facts.push_back({"From", card.installed ? std::string("Your Mods folder, switched off") : std::string("Thunderstore")});
|
||||
// A download or an update. A mod that is here and only switched off has nothing to fetch.
|
||||
if (asking && !card.installed && card.bytes) facts.push_back({"Size", megabytes(card.bytes)});
|
||||
|
||||
// The controls under the card: the pad's own names for its buttons when one is connected.
|
||||
dingosdk::ControllerInput pad;
|
||||
|
||||
@@ -346,6 +346,7 @@ struct MapDownloadCard {
|
||||
std::size_t step{}, steps{}; // applying: the merge's steps done of all
|
||||
std::string step_name;
|
||||
bool server{}, installed{}, moved{}, choice{true};
|
||||
std::uint64_t bytes{}; // asking: the package's size; 0 when it is not known
|
||||
std::uint64_t received{}, total{};
|
||||
std::uint32_t yes_bind{}, no_bind{};
|
||||
};
|
||||
|
||||
@@ -233,6 +233,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() {
|
||||
card.moved = view.moved;
|
||||
card.installed = view.installed;
|
||||
card.have = view.have;
|
||||
card.bytes = view.bytes;
|
||||
card.choice = view.choice;
|
||||
card.received = view.received;
|
||||
card.total = view.total;
|
||||
|
||||
Reference in new issue
Block a user