Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ I've focused on modded lobby joining and made the Online menus easier to use.
- Live offline FOV adjustment, including paused races, with the corrected guest
register handling. Existing online restrictions stay.
- Windows updater log handoff and better session diagnostics.
- Fixed installed Store mods returning to Needs Attention after restarting.
Existing completed entries are recovered without downloading them again.
- Integrated the patch guardrail setup and follow-ups from PRs #76–#78. The full
source consolidation remains separate.
- Updated current guides and prepared Windows x64, both Linux AppImages and the
Expand Down
2 changes: 2 additions & 0 deletions docs/DKR-R-v1.0.12-Changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ FOV slider so you can see it changing while you play.
- Added remembered Discord sign-in on Linux and Steam Deck without a keyring.
- Made the offline FOV slider work live, including while paused.
- Fixed logging after an updater restart and improved join diagnostics.
- Fixed installed Store mods showing up in Needs Attention after a restart.
You won't need to download those installed mods again.

All the existing memory, HUD, modded lobby and spectator fixes are kept.
Your saves, mods and settings can stay. Please use the complete v1.0.12
Expand Down
2 changes: 2 additions & 0 deletions docs/PUBLIC-1.0.12-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ FOV slider so you can see it changing while you play.
- Added remembered Discord sign-in on Linux and Steam Deck without a keyring.
- Made the offline FOV slider work live, including while paused.
- Fixed logging after an updater restart and improved join diagnostics.
- Fixed installed Store mods showing up in Needs Attention after a restart.
You won't need to download those installed mods again.

All the existing memory, HUD, modded lobby and spectator fixes are kept.
Your saves, mods and settings can stay. Please use the complete v1.0.12
Expand Down
5 changes: 5 additions & 0 deletions patches/character-library-portraits/ReleaseOverlay.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ if(PROJECT_NAME STREQUAL "DKRRuntimePreparation")
file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*" "${auth}/tests/*")
file(GLOB profile_authored CONFIGURE_DEPENDS "${DKRPORT_ROOT}/runtime-recomp/src/game/online_profile/*")
list(APPEND authored ${profile_authored})
list(APPEND authored "${DKRPORT_ROOT}/runtime-recomp/src/game/mod_store/download_history.hpp")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${authored} "${auth}/pipeline.py" "${auth}/ReleaseOverlay.cmake")
execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result)
if(NOT result EQUAL 0)
Expand Down Expand Up @@ -75,6 +76,10 @@ if(PROJECT_NAME STREQUAL "DKRRuntimePreparation")
add_executable(DKRProfileChecks EXCLUDE_FROM_ALL "${DKRPORT_ROOT}/runtime-recomp/tests/online_profile_tests.cpp")
target_compile_features(DKRProfileChecks PRIVATE cxx_std_20)
target_include_directories(DKRProfileChecks PRIVATE "${DKRPORT_ROOT}/runtime-recomp/src/game")
add_executable(DKRModStoreHistoryChecks EXCLUDE_FROM_ALL "${DKRPORT_ROOT}/runtime-recomp/tests/mod_store_history_tests.cpp")
target_compile_features(DKRModStoreHistoryChecks PRIVATE cxx_std_20)
target_include_directories(DKRModStoreHistoryChecks PRIVATE "${DKRPORT_ROOT}/runtime-recomp/src/game"
"${DKRPORT_ROOT}/extern/rt64/src/contrib")
get_target_property(links DKRPortGame LINK_DIRECTORIES)
if(links)
target_link_directories(DKRCharacterPortraitChecks PRIVATE ${links})
Expand Down
28 changes: 28 additions & 0 deletions patches/character-library-portraits/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,34 @@ def friends_layout(text,out):
return text[:grid_start]+grid+text[grid_end:]

def account_service(text,out):
# Extend the retained service at its existing composition point. The history
# policy is normal first-party source, shared with the ROM-free host tests.
history=ROOT/'runtime-recomp/src/game/mod_store/download_history.hpp'
text='#include "'+history.as_posix()+'"\n'+text
begin=text.index(' Json rows = Json::array();',text.index(' void persist_downloads()'))
end=text.index('\n }',begin)
text=text[:begin]+''' Json saved;
{
std::lock_guard lock(mutex);
saved = dkr::store::history::save<Json>(state.downloads);
}
write_json(root / "downloads.json", saved);'''+text[end:]
text=once(text,' const auto saved = read_json(p.root / "downloads.json");',
''' const auto saved = read_json(p.root / "downloads.json");
const auto receipts = read_json(p.root / "installations.json");
auto restored = dkr::store::history::restore<Download>(saved, receipts, [&](const std::string& path) {
return dkr::store::history::member_present(p.root.parent_path(), path);
});''')
begin=text.index(' if (saved.contains("jobs")')
end=text.index('\n });',begin)
text=text[:begin]+' s.downloads = std::move(restored);'+text[end:]
text=once(text,' s.downloads = std::move(restored);\n });',
''' s.downloads = std::move(restored);
});
if (saved.is_object() && saved.contains("jobs") && saved["jobs"].is_array() &&
saved["jobs"].size() <= 64 && (!saved.contains("schema") || saved["schema"] == 1 || saved["schema"] == 2)) {
try { p.persist_downloads(); } catch (const std::exception& error) { p.fail(error); }
}''')
text=once(text,'namespace dkr::store {',
'#include "'+(out/'display_name_save.hpp').as_posix()+'"\n#include "'+(out/'profile_bridge.hpp').as_posix()+'"\n#include "'+(out/'picture_codec.hpp').as_posix()+'"\n#include "'+(out/'profile_retry.hpp').as_posix()+'"\n#define DKR_PROFILE_WITH_KEYRING 1\n#include "'+(out/'linux_device_store.hpp').as_posix()+'"\n\nnamespace dkr::store {')
for operation in ('store','load','clear'):
Expand Down
2 changes: 1 addition & 1 deletion patches/release-prep/v1.0.11/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
names=['DKRPortGame',*[v[0] for v in TARGETS.values()],'DKRVehicleOriginalChecks','DKRVehicleOriginalBeforeChecks']
names += [f'DKROnlineMenu{r}{s}Checks' for r in (77,80) for s in ('Before','')]
names += ['DKROnlineSetupChecks','DKROnlinePartyChecks','DKRDirectoryLifecycleChecks',
'DKRCharacterPortraitChecks','DKRProfileChecks']
'DKRCharacterPortraitChecks','DKRProfileChecks','DKRModStoreHistoryChecks']
if a.sanitizer:names.append('DKRVehicleOriginalSanitizerChecks')
a.log.parent.mkdir(parents=True,exist_ok=True)
with a.log.open('w') as out:subprocess.run([a.cmake,'--build',str(a.build),'--config','RelWithDebInfo','--target',*names,'--parallel',str(a.jobs)],stdout=out,stderr=subprocess.STDOUT,check=True)
Expand Down
6 changes: 6 additions & 0 deletions runtime-recomp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,12 @@ if(BUILD_TESTING)
"${DKR_DEPENDENCIES_DIR}/n64-modern-runtime/N64Recomp/include")
add_test(NAME DKRLiveFov COMMAND DKRLiveFovTests)

add_executable(DKRModStoreHistoryTests tests/mod_store_history_tests.cpp)
target_compile_features(DKRModStoreHistoryTests PRIVATE cxx_std_20)
target_include_directories(DKRModStoreHistoryTests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/src/game"
"${DKRPORT_ROOT}/extern/rt64/src/contrib")
add_test(NAME DKRModStoreHistory COMMAND DKRModStoreHistoryTests)

add_test(NAME DKRTestAssertionContract
COMMAND "${CMAKE_COMMAND}"
"-DTEST_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}/test-assertion-contract"
Expand Down
161 changes: 161 additions & 0 deletions runtime-recomp/src/game/mod_store/download_history.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#pragma once

#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace dkr::store::history {
inline constexpr std::uint64_t max_package_bytes = 2ULL * 1024 * 1024 * 1024;

template <class Json> std::string field(const Json& value, const char* name, std::size_t limit) {
if (!value.is_object())
return {};
const auto it = value.find(name);
if (it == value.end() || !it->is_string())
return {};
const auto& text = it->template get_ref<const std::string&>();
return text.size() <= limit ? text : std::string{};
}

inline bool identifier(std::string_view value) {
return !value.empty() && value.size() <= 160 && std::all_of(value.begin(), value.end(), [](unsigned char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
});
}

inline bool digest(std::string_view value) {
return value.size() == 64 && std::all_of(value.begin(), value.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
}

template <class Json> std::uint64_t bytes(const Json& value) {
if (!value.is_object())
return 0;
const auto it = value.find("bytes");
if (it == value.end() || !it->is_number_unsigned())
return 0;
const auto result = it->template get<std::uint64_t>();
return result <= max_package_bytes ? result : 0;
}

// Only used by the background history loader. This never claims ownership,
// hashes whole packages, changes files or replaces native-library repair checks.
inline std::filesystem::path utf8_path(std::string_view value) {
return std::filesystem::path(std::u8string(value.begin(), value.end()));
}

inline bool member_present(const std::filesystem::path& config, const std::string& relative) {
if (relative.empty() || relative.find_first_of("\\:") != relative.npos)
return false;
const auto path = utf8_path(relative);
if (path.is_absolute() || path.has_root_name())
return false;
auto at = config;
std::error_code error;
for (const auto& part : path) {
if (part.empty() || part == "." || part == "..")
return false;
at /= part;
const auto status = std::filesystem::symlink_status(at, error);
if (error || std::filesystem::is_symlink(status) ||
(!std::filesystem::is_directory(status) && !std::filesystem::is_regular_file(status)))
return false;
}
return true;
}

template <class Job, class Json, class Present>
const Json* completed_receipt(const Job& job, const Json& registry, Present&& present) {
if (!registry.is_object() || !registry.contains("schema") || registry["schema"] != 1 ||
!registry.contains("records") || !registry["records"].is_array())
return nullptr;
// Explicit replacement/update attempts may be paused even when an older
// successful install of that release exists. Leave those attempts alone.
if (!job.intent.empty() || !job.instance.empty())
return nullptr;
for (const auto& record : registry["records"]) {
if (field(record, "item", 160) != job.item || field(record, "release", 160) != job.release ||
field(record, "category", 32) != job.category || field(record, "state", 80) != "Installed" ||
(record.contains("stored") && record["stored"] != false))
continue;
const auto hash = field(record, "digest", 64);
if (!digest(hash) || (!job.digest.empty() && job.digest != hash) || !record.contains("members") ||
!record["members"].is_array() || record["members"].empty())
continue;
const bool complete = std::all_of(record["members"].begin(), record["members"].end(), [&](const Json& member) {
return !field(member, "key", 512).empty() && !field(member, "kind", 40).empty() &&
present(field(member, "path", 4096));
});
if (complete)
return &record;
}
return nullptr;
}

template <class Job, class Json, class Present>
std::vector<Job> restore(const Json& saved, const Json& registry, Present&& present) {
std::vector<Job> result;
if (!saved.is_object() || !saved.contains("jobs") || !saved["jobs"].is_array() || saved["jobs"].size() > 64)
return result;
const bool legacy = !saved.contains("schema") || saved["schema"] == 1;
if (!legacy && saved["schema"] != 2)
return result;
for (const auto& row : saved["jobs"]) {
Job job;
job.item = field(row, "item", 160);
job.release = field(row, "release", 160);
job.title = field(row, "title", 200);
job.category = field(row, "category", 32);
job.version = field(row, "version", 80);
job.intent = field(row, "intent", 32);
job.instance = field(row, "instance", 512);
if (!identifier(job.item) || !identifier(job.release))
continue;
job.key = job.item + "_" + job.release + (job.instance.empty() ? "" : "_" + job.instance);
if (std::any_of(result.begin(), result.end(), [&](const Job& old) { return old.key == job.key; }))
continue;
const auto hash = field(row, "sha256", 64);
if (digest(hash))
job.digest = hash;
job.total = bytes(row);
const auto previous = field(row, "state", 80);
const Json* receipt = nullptr;
if (legacy && previous == "Paused")
receipt = completed_receipt(job, registry, present);
if (previous == "Installed" || receipt) {
job.state = "Installed";
if (receipt) {
job.digest = field(*receipt, "digest", 64);
if (!job.total)
job.total = bytes(*receipt);
}
job.received = job.total;
} else {
// Resuming always obtains current integrity metadata and verifies
// the cache. No filename or queued/active state is trusted on boot.
job.state = previous == "Failed" || previous == "Install failed" ? "Failed" : "Paused";
job.error = field(row, "error", 2000);
if (job.error.empty())
job.error = previous == "Install failed" ? "Installation did not finish. Resume to verify the saved package."
: "Resume to check or reuse the saved download.";
}
result.push_back(std::move(job));
}
return result;
}

template <class Json, class Jobs> Json save(const Jobs& jobs) {
Json rows = Json::array();
for (const auto& job : jobs)
rows.push_back({{"item", job.item}, {"release", job.release}, {"title", job.title},
{"category", job.category}, {"version", job.version}, {"state", job.state},
{"sha256", job.digest}, {"bytes", job.total}, {"intent", job.intent},
{"instance", job.instance}, {"error", job.error.size() <= 2000 ? job.error : std::string{}}});
return {{"schema", 2}, {"jobs", std::move(rows)}};
}
} // namespace dkr::store::history
Loading
Loading