Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
76e01e5
Fix 32-bit stat overflow on btrfs for token file reads
Selectively11 Jun 5, 2026
214796d
Show LUA games as non-Steam game in friends status
Selectively11 Jun 5, 2026
5f95c68
Add Show Game in Friends toggle for non-Steam game display
Selectively11 Jun 5, 2026
be45eb6
Respect Mark as Private for games in friends now-playing spoof
Selectively11 Jun 6, 2026
2f9238b
2.2.0 experimental 1
Selectively11 Jun 7, 2026
22a0c68
Harden mixed-root quota guard against compounding
Selectively11 Jun 7, 2026
4ec9a9b
Add CR_SetApps export to atomically sync namespace app set
Selectively11 Jun 7, 2026
182937a
Reorganize Settings and restore the Cleanup tab
Selectively11 Jun 7, 2026
7e9cc7a
Add native stats store and RPC handlers
Selectively11 Jun 7, 2026
427a69f
Add Stats and Playtime page with cloud-backed data
Selectively11 Jun 7, 2026
3156ec9
Import native achievements and fetch missing schemas
Selectively11 Jun 8, 2026
476e323
Add optional pre-release version suffix (2.2.0-TEST1)
Selectively11 Jun 8, 2026
ac91470
Add async cloud upload and per-platform playtime tracking
Selectively11 Jun 9, 2026
aef76e9
Sync stats, achievements, and playtime across devices
Selectively11 Jun 9, 2026
27ac5ce
Gate stats sync behind per-feature toggles
Selectively11 Jun 9, 2026
8a58bb6
Add Linux UI controls for stats sync
Selectively11 Jun 9, 2026
dacc503
Add multi-root cloud sync, native-faithful eviction, and Steam 178104…
Selectively11 Jun 11, 2026
84be7cf
Fix Linux KV injector crash on Steam 1781043450 with updated RVAs and…
Selectively11 Jun 11, 2026
d82425b
Fix cloud-sync arrows, exit-sync latency, and the game-exit crash
Selectively11 Jun 12, 2026
8df6381
Fix sync hang, cross-device achievement sync, playtime migration, and…
Selectively11 Jun 12, 2026
a837f85
2.2.0 Test-3
Selectively11 Jun 14, 2026
c23ce35
2.2.0 Test 11
Selectively11 Jun 21, 2026
f023363
feature/git-action: CI workflow, build fixes, and master branch updates
MohandL3G Jun 22, 2026
4d19f90
Merge master into feature/git-action
MohandL3G Jun 28, 2026
1271fd1
Dynamic Steam controls: show Restart/Close when running, Start when s…
MohandL3G Jun 29, 2026
83368d5
Merge branch 'fix/steam-detection' into feature/git-action
MohandL3G Jun 29, 2026
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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shell scripts must use LF endings or they break when run on Linux (bash\r).
*.sh text eol=lf
26 changes: 26 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build

on: [push, pull_request, workflow_dispatch]

jobs:
build-windows:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4

- uses: microsoft/setup-msbuild@v2

- name: Configure CMake
run: cmake -B build -G "Visual Studio 17 2022" -A x64

- name: Build
run: cmake --build build --config Release

- uses: actions/upload-artifact@v4
with:
name: cloudredirect-windows
path: |
build/Release/cloud_redirect.dll
build/Release/cloud_redirect_cli.exe
build/Release/cloud760_tool.exe
ui/bin/publish/CloudRedirect.exe
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
build/
build-*/
dist/
ui/bin/
ui/obj/
ui/publish/
ui-linux/build/
flatpak/.flatpak-builder/
flatpak/build-dir/
flatpak/repo/
flatpak/cloud_redirect.so
build.ps1
ui/Resources/cloud_redirect_cli.exe
ui/Resources/payloads/
tests/
src/testutil/
docs/
tools/
flatpak/release.sh
ui/native/
70 changes: 58 additions & 12 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
file(READ "${CMAKE_SOURCE_DIR}/Version.props" VERSION_PROPS_CONTENT)
string(REGEX MATCH "<ReleaseVersion>([^<]+)</ReleaseVersion>" _ "${VERSION_PROPS_CONTENT}")
set(CR_RELEASE_VERSION "${CMAKE_MATCH_1}")
string(REGEX MATCH "<ReleasePrerelease>([^<]*)</ReleasePrerelease>" _ "${VERSION_PROPS_CONTENT}")
set(CR_PRERELEASE "${CMAKE_MATCH_1}")

if(NOT CR_RELEASE_VERSION)
set(CR_RELEASE_VERSION "0.0.0")
endif()
set(CR_RELEASE_VERSION "${CR_RELEASE_VERSION}${CR_PRERELEASE}")

# ── Generate version string with git SHA ────────────────────────────────
# Release builds may pass -DCR_GIT_SHA to pin the build id (e.g. when the build
Expand Down Expand Up @@ -51,8 +54,8 @@ set(CR_SO_VERSION "${CR_RELEASE_VERSION}+${GIT_SHA}")
set(COMMON_SOURCES
src/common/protobuf.cpp
src/common/json.cpp
src/common/coop_yield.cpp
src/common/vdf.cpp
src/common/bkv_stats.cpp
src/common/remotecache_repair.cpp
src/common/manifest_store.cpp
src/common/app_state.cpp
Expand All @@ -69,10 +72,11 @@ set(COMMON_SOURCES
src/common/cli.cpp
src/common/cloud_provider_base.cpp
src/common/autocloud_scan.cpp
src/common/autocloud_bootstrap.cpp
src/common/steam_kv_injector.cpp
src/common/parental_bypass.cpp
src/common/metadata_sync.cpp
src/common/stats_store.cpp
src/common/stats_handlers.cpp
src/common/miniz.c
src/common/miniz_tdef.c
src/common/miniz_tinfl.c
Expand Down Expand Up @@ -107,6 +111,12 @@ else()
src/platform/linux/vtable_hook.cpp
src/platform/linux/cloud_hooks.cpp
src/platform/linux/cloud_intercept.cpp
src/platform/linux/stats_hooks.cpp
src/platform/linux/gamesplayed_hook.cpp
src/platform/linux/live_playtime.cpp
src/platform/linux/achievement_inject.cpp
src/platform/linux/schema_fetch.cpp
src/platform/linux/recvpkt_hook.cpp
src/platform/linux/http_transport_linux.cpp
src/platform/linux/token_store_linux.cpp
src/platform/linux/log.cpp
Expand Down Expand Up @@ -151,12 +161,19 @@ if(WIN32)
target_link_libraries(cloud_redirect_cli PRIVATE Shell32 Ole32)
# Ensure DLL is built before CLI
add_dependencies(cloud_redirect_cli cloud_redirect)

# Standalone Steam Cloud file manager for a single AppID (default 760).
# Self-contained: resolves the steam_api64.dll flat exports at runtime, so it
# needs no Steamworks SDK headers/libs. steam_api64.dll must sit next to it.
add_executable(cloud760_tool
src/platform/win/cloud760_tool.cpp
)
else()
target_include_directories(cloud_redirect PRIVATE src/platform/linux)
target_link_libraries(cloud_redirect PRIVATE dl pthread)
# Steam on Linux is 32-bit; use LINUX_32BIT=ON when cross-compiling on 64-bit host
# Match Steam's pre-C++11 ABI
target_compile_definitions(cloud_redirect PRIVATE _GLIBCXX_USE_CXX11_ABI=0)
target_compile_definitions(cloud_redirect PRIVATE _GLIBCXX_USE_CXX11_ABI=0 _FILE_OFFSET_BITS=64)
if(LINUX_32BIT)
target_compile_options(cloud_redirect PRIVATE -m32)
target_link_options(cloud_redirect PRIVATE -m32)
Expand Down Expand Up @@ -200,6 +217,7 @@ endif()
enable_testing()

if(EXISTS ${CMAKE_SOURCE_DIR}/tests/autocloud_native_tests.cpp)
add_compile_definitions(CR_RELEASE_VERSION="${CR_RELEASE_VERSION}")
if(WIN32)
add_executable(autocloud_native_tests
tests/autocloud_native_tests.cpp
Expand Down Expand Up @@ -235,14 +253,6 @@ if(EXISTS ${CMAKE_SOURCE_DIR}/tests/autocloud_native_tests.cpp)
target_include_directories(remotecache_repair_tests PRIVATE src/common)
add_test(NAME remotecache_repair_tests COMMAND remotecache_repair_tests)

if(EXISTS ${CMAKE_SOURCE_DIR}/tests/bkv_stats_tests.cpp)
add_executable(bkv_stats_tests
tests/bkv_stats_tests.cpp
src/common/bkv_stats.cpp
)
target_include_directories(bkv_stats_tests PRIVATE src/common)
add_test(NAME bkv_stats_tests COMMAND bkv_stats_tests)
endif()

add_executable(rpc_handlers_tests
tests/rpc_handlers_tests.cpp
Expand Down Expand Up @@ -272,6 +282,7 @@ if(EXISTS ${CMAKE_SOURCE_DIR}/tests/autocloud_native_tests.cpp)
src/common/legacy_metadata_cleanup.cpp
src/common/manifest_store.cpp
src/common/app_state.cpp
src/common/coop_yield.cpp
src/common/token_store.cpp
src/common/cloud_staging.cpp
src/common/pending_ops_journal.cpp
Expand Down Expand Up @@ -346,15 +357,50 @@ if(EXISTS ${CMAKE_SOURCE_DIR}/tests/autocloud_native_tests.cpp)
)
target_include_directories(vdf_tests PRIVATE src/common)
add_test(NAME vdf_tests COMMAND vdf_tests)

add_executable(stats_store_tests
tests/stats_store_tests.cpp
src/common/stats_store.cpp
src/common/json.cpp
src/common/metadata_sync.cpp
src/common/vdf.cpp
)
target_include_directories(stats_store_tests PRIVATE src/common)
if(WIN32)
target_include_directories(stats_store_tests PRIVATE src/platform/win)
target_sources(stats_store_tests PRIVATE
src/platform/win/log.cpp
src/platform/win/platform_win.cpp
)
target_link_libraries(stats_store_tests PRIVATE Shlwapi Advapi32 Crypt32 Shell32 Ole32)
else()
target_include_directories(stats_store_tests PRIVATE src/platform/linux)
target_sources(stats_store_tests PRIVATE
src/platform/linux/log.cpp
src/platform/linux/platform_linux.cpp
src/platform/linux/file_util.cpp
)
target_link_libraries(stats_store_tests PRIVATE pthread dl)
endif()
add_test(NAME stats_store_tests COMMAND stats_store_tests)
endif()

# ── UI (Windows only) ───────────────────────────────────────────────────

if(WIN32)
# The csproj expects the DLL/CLI at ../build/Release/; copy from the actual
# build output dir so out-of-tree builds (build-win) work correctly.
add_custom_target(ui ALL
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/build/Release"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:cloud_redirect>"
"${CMAKE_SOURCE_DIR}/build/Release/cloud_redirect.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:cloud_redirect_cli>"
"${CMAKE_SOURCE_DIR}/build/Release/cloud_redirect_cli.exe"
COMMAND dotnet publish -c Release -r win-x64 --self-contained false -o bin/publish
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/ui
COMMENT "Publishing CloudRedirect UI"
DEPENDS cloud_redirect
DEPENDS cloud_redirect cloud_redirect_cli
)
endif()
4 changes: 4 additions & 0 deletions Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<PropertyGroup>
<!-- Shared user-facing version (X.Y.Z) -->
<ReleaseVersion>2.2.4</ReleaseVersion>

<!-- Optional pre-release suffix (e.g. -TEST1, -beta). Empty for stable releases.
Appended to the user-facing version shown in Settings; AssemblyVersion stays numeric. -->
<ReleasePrerelease></ReleasePrerelease>

<!-- Sync engine generation - increment on breaking protocol changes -->
<CoreGeneration>1.0</CoreGeneration>
Expand Down
2 changes: 1 addition & 1 deletion cli-rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cloudredirect-cli"
version = "2.2.2"
version = "2.1.8"
edition = "2021"
description = "CloudRedirect CLI (STFixer) — Rust port"

Expand Down
8 changes: 0 additions & 8 deletions cli-rust/src/embedded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ const PAYLOADS: &[(i64, &[u8])] = &[
1781041600,
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/embedded/payloads/1781041600/payload")),
),
(
1782257239,
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/embedded/payloads/1782257239/payload")),
),
(
1782344391,
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/embedded/payloads/1782344391/payload")),
),
];

pub fn dll_available() -> bool {
Expand Down
92 changes: 18 additions & 74 deletions cli-rust/src/patcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ impl Patcher {

// 2. Payload cache.
let mut log = |m: &str| println!("{}", m);
let mut cache_path = match crypto::find_cache_path(&self.steam_path, &mut log) {
let cache_path = match crypto::find_cache_path(&self.steam_path, &mut log) {
Some(p) => p,
None => {
self.log("Payload cache not found. Deploying embedded payload..");
Expand All @@ -222,26 +222,12 @@ impl Patcher {
};

self.log("Patching payload (offline setup)..");
let (mut payload, mut iv) = match self.read_and_decrypt_payload(&cache_path) {
let (payload, iv) = match self.read_and_decrypt_payload(&cache_path) {
Ok(v) => v,
Err(e) => return PatchOutcome::fail(e),
};

let mut resolved_setup = self.resolve_setup_patch_offsets(&payload);

// Payload may be corrupted by a prior CR version; try the embedded copy.
if resolved_setup.as_ref().map_or(true, |r| r.is_empty()) {
if let Some((rec_path, rec_payload, rec_iv)) =
self.try_recover_corrupted_payload(&cache_path, version)
{
cache_path = rec_path;
payload = rec_payload;
iv = rec_iv;
resolved_setup = self.resolve_setup_patch_offsets(&payload);
}
}

let resolved_setup = match resolved_setup {
let resolved_setup = match self.resolve_setup_patch_offsets(&payload) {
Some(r) if !r.is_empty() => r,
_ => {
return PatchOutcome::fail(
Expand Down Expand Up @@ -397,36 +383,33 @@ impl Patcher {
/// Resolve payload section bounds: .text and the first non-standard ("obf") section.
fn resolve_payload_sections(&self, payload: &[u8]) -> Option<(i64, i64, i64, i64)> {
let sections = PeSection::parse(payload);
let text = PeSection::find(&sections, ".text")?;
let text = PeSection::find(&sections, ".text");

const KNOWN: [&str; 8] = [
".text", ".rdata", ".data", ".pdata", ".fptable", ".rsrc", ".reloc", ".idata",
const KNOWN: [&str; 7] = [
".text", ".rdata", ".data", ".pdata", ".fptable", ".rsrc", ".reloc",
];
let obf = sections.iter().find(|s| !KNOWN.contains(&s.name.as_str()));

let t_start = text.raw_offset as i64;
let t_end = (t_start + text.raw_size as i64).min(payload.len() as i64);
// Fall back to .text bounds if no obfuscated section.
let (g_start, g_end) = match obf {
Some(o) => {
let s = o.raw_offset as i64;
(s, (s + o.raw_size as i64).min(payload.len() as i64))
let (text, obf) = match (text, obf) {
(Some(t), Some(o)) => (t, o),
_ => {
self.log(" Payload: missing expected sections");
return None;
}
None => (t_start, t_end),
};

let t_start = text.raw_offset as i64;
let t_end = (t_start + text.raw_size as i64).min(payload.len() as i64);
let g_start = obf.raw_offset as i64;
let g_end = (g_start + obf.raw_size as i64).min(payload.len() as i64);
Some((t_start, t_end, g_start, g_end))
}

fn resolve_setup_patch_offsets(&self, payload: &[u8]) -> Option<Vec<PatchEntry>> {
let (t_start, t_end, g_start, g_end) = self.resolve_payload_sections(payload)?;
let mut log = |m: &str| println!("{}", m);
// V2 first, V1 fallback.
let new_defs = signatures::payload_setup_defs();
if let Some(r) = signatures::resolve_pattern_group(payload, &new_defs, t_start, t_end, g_start, g_end, &mut log) {
return Some(r);
}
let old_defs = signatures::payload_setup_defs_v1();
signatures::resolve_pattern_group(payload, &old_defs, t_start, t_end, g_start, g_end, &mut log)
let defs = signatures::payload_setup_defs();
signatures::resolve_pattern_group(payload, &defs, t_start, t_end, g_start, g_end, &mut log)
}

/// Read + AES-decrypt + zlib-inflate the payload cache. Returns (payload, iv).
Expand Down Expand Up @@ -483,45 +466,6 @@ impl Patcher {
embedded::install_payload(&self.steam_path, version, |m| println!("{}", m))
}

// Sideline a corrupted cache file, deploy the embedded payload, and re-decrypt.
fn try_recover_corrupted_payload(
&self,
old_path: &Path,
version: i64,
) -> Option<(PathBuf, Vec<u8>, [u8; 16])> {
self.log(" Payload appears corrupted by a previous version. Attempting recovery...");

let corrupt = file_util::with_extension_suffix(old_path, ".corrupt");
if let Err(e) = std::fs::rename(old_path, &corrupt) {
self.log(&format!(" Could not rename corrupted payload: {}", e));
return None;
}
self.log(&format!(" Corrupted payload saved to {}", corrupt.display()));

let new_path = match self.deploy_embedded_payload(version) {
Some(p) => p,
None => {
self.log(" Recovery failed: no embedded payload for this build.");
let _ = std::fs::rename(&corrupt, old_path);
return None;
}
};

match self.read_and_decrypt_payload(&new_path) {
Ok((payload, iv)) => {
self.log(&format!(
" Recovery succeeded: clean payload deployed ({} bytes)",
payload.len()
));
Some((new_path, payload, iv))
}
Err(e) => {
self.log(&format!(" Recovery failed: {}", e));
None
}
}
}

fn backup(&self, path: &Path) {
let orig = file_util::with_extension_suffix(path, ".orig");
if !orig.exists() {
Expand Down
Loading