#include "offline_steam.h" #include "Engine/Core/Hooks/hooks.h" #include "Engine/Core/Log/logging.h" #include "Engine/Core/Platform/launcher_support.h" #include #include #include #include #include #include #include #include #include #include #include namespace dingosdk::offline_steam { namespace { // Interfaces Skate.exe requests by version string. Every method of every // interface is a stub that returns 0 and logs its first call; the overrides // below give the methods the game depends on real answers. Slot numbers come // from the vtable offsets in the shipped steam_api64.dll flat exports. enum Interface : int { user, friends, utils, matchmaking, matchmaking_servers, networking, networking_messages, networking_sockets, networking_utils, input, controller, parties, game_search, apps, user_stats, remote_storage, remote_play, unknown, interface_count }; constexpr std::array interface_names{ "SteamUser", "SteamFriends", "SteamUtils", "SteamMatchMaking", "SteamMatchMakingServers", "SteamNetworking", "SteamNetworkingMessages", "SteamNetworkingSockets", "SteamNetworkingUtils", "SteamInput", "SteamController", "SteamParties", "SteamMatchGameSearch", "SteamApps", "SteamUserStats", "SteamRemoteStorage", "SteamRemotePlay", "unknown"}; constexpr std::size_t slot_count = 128; constexpr std::uint32_t app_id = 3354750; constexpr char empty[] = ""; std::array, slot_count>, interface_count> called{}; void note(int interface, std::size_t slot) noexcept { if (called[interface][slot].exchange(true)) return; try { logging::log(logging::Level::info, logging::Channel::network, "Offline Steam: first call to {} slot {}", interface_names[interface], slot); } catch (...) {} } template std::uint64_t stub(void*) noexcept { note(I, S); return 0; } template std::array make_table(std::index_sequence) { #pragma warning(push) #pragma warning(disable: 4191) return {reinterpret_cast(&stub)...}; #pragma warning(pop) } struct Object { void** vtable; std::array padding{}; }; template std::array table = make_table(std::make_index_sequence{}); std::array objects{}; // ---------------------------------------------------------------- callbacks // Same declaration as Steamworks' CCallbackBase, so MSVC lays the vtable out // exactly as the game's compiled callbacks expect. class CallbackBase { public: virtual void Run(void* param) = 0; virtual void Run(void* param, bool io_failure, std::uint64_t call) = 0; virtual int GetCallbackSizeBytes() = 0; std::uint8_t flags; int id; }; std::mutex callback_mutex; std::vector callbacks; std::deque>> pending; template void post(int id, const T& payload) { std::vector bytes(sizeof(T)); std::memcpy(bytes.data(), &payload, sizeof(T)); std::lock_guard lock(callback_mutex); pending.emplace_back(id, std::move(bytes)); } void __cdecl register_callback(CallbackBase* callback, int id) { if (!callback) return; callback->flags |= 1; callback->id = id; std::lock_guard lock(callback_mutex); callbacks.push_back(callback); } void __cdecl unregister_callback(CallbackBase* callback) { if (!callback) return; callback->flags &= static_cast(~1u); std::lock_guard lock(callback_mutex); std::erase(callbacks, callback); } void __cdecl register_call_result(void*, std::uint64_t) {} void __cdecl unregister_call_result(void*, std::uint64_t) {} void __cdecl run_callbacks() { for (;;) { std::pair> next; std::vector targets; { std::lock_guard lock(callback_mutex); if (pending.empty()) return; next = std::move(pending.front()); pending.pop_front(); for (auto* callback : callbacks) if (callback->id == next.first) targets.push_back(callback); } for (auto* callback : targets) callback->Run(next.second.data()); } } // ---------------------------------------------------------------- overrides using Uint64Out = std::uint64_t*; // Methods returning CSteamID use MSVC's hidden return pointer (after this). Uint64Out zero_steam_id(void*, Uint64Out out) noexcept { if (out) *out = 0; return out; } const char* empty_string(void*) noexcept { return empty; } std::uint64_t one(void*) noexcept { return 1; } // ISteamUser Uint64Out user_steam_id(void*, Uint64Out out) noexcept { if (out) *out = steam_id(); return out; } std::atomic next_ticket{1}; #pragma pack(push, 8) struct AuthSessionTicketResponse { std::uint32_t ticket; int result; }; struct WebApiTicketResponse { std::uint32_t ticket; int result; int size; std::uint8_t data[2560]; }; #pragma pack(pop) void fill_ticket(std::uint8_t* data, std::uint32_t size) noexcept { const auto id = steam_id(); for (std::uint32_t i = 0; i < size; ++i) data[i] = static_cast(id >> (8 * (i % 8))); } std::uint32_t auth_session_ticket(void*, void* ticket, int capacity, std::uint32_t* size, const void*) noexcept { const auto handle = next_ticket.fetch_add(1); const auto length = static_cast(capacity >= 64 ? 64 : (capacity > 0 ? capacity : 0)); if (ticket) fill_ticket(static_cast(ticket), length); if (size) *size = length; try { post(163, AuthSessionTicketResponse{handle, 1}); } catch (...) {} return handle; } std::uint32_t web_api_ticket(void*, const char*) noexcept { const auto handle = next_ticket.fetch_add(1); try { auto response = std::make_unique(); response->ticket = handle; response->result = 1; response->size = 64; fill_ticket(response->data, 64); post(168, *response); } catch (...) {} return handle; } // ISteamFriends const char* persona_name(void*) noexcept { return launcher::offline_player_name; } // ISteamUtils std::uint64_t server_time(void*) noexcept { return static_cast(std::time(nullptr)); } const char* country(void*) noexcept { return "US"; } const char* language(void*) noexcept { return "english"; } const char* language_list(void*) noexcept { return "english"; } std::uint64_t battery(void*) noexcept { return 255; } std::uint64_t app(void*) noexcept { return app_id; } // ISteamNetworkingUtils std::uint64_t local_timestamp(void*) noexcept { LARGE_INTEGER counter{}, frequency{}; QueryPerformanceCounter(&counter); QueryPerformanceFrequency(&frequency); return static_cast(counter.QuadPart / frequency.QuadPart * 1000000 + counter.QuadPart % frequency.QuadPart * 1000000 / frequency.QuadPart) + 1000000; } template void set(std::size_t slot, auto function) { #pragma warning(push) #pragma warning(disable: 4191) table[slot] = reinterpret_cast(function); #pragma warning(pop) } void build_objects() { set(0, &one); // GetHSteamUser set(1, &one); // BLoggedOn set(2, &user_steam_id); // GetSteamID set(13, &auth_session_ticket); // GetAuthSessionTicket set(14, &web_api_ticket); // GetAuthTicketForWebApi set(24, &one); // GetPlayerSteamLevel set(0, &persona_name); // GetPersonaName set(2, &one); // GetPersonaState: online for (const auto slot : {4, 19, 25, 39, 41, 51, 57}) set(slot, &zero_steam_id); for (const auto slot : {7, 9, 14, 20, 21, 45, 47, 78}) set(slot, &empty_string); set(43, &one); // SetRichPresence set(2, &one); // GetConnectedUniverse: public set(3, &server_time); // GetServerRealTime set(4, &country); // GetIPCountry set(8, &battery); // GetCurrentBatteryPower: AC set(9, &app); // GetAppID set(23, &language); // GetSteamUILanguage for (const auto slot : {12, 18, 35}) set(slot, &zero_steam_id); for (const auto slot : {19, 24}) set(slot, &empty_string); set(0, &one); // BIsSubscribed set(4, &language); // GetCurrentGameLanguage set(5, &language_list); // GetAvailableGameLanguages set(6, &one); // BIsSubscribedApp set(19, &one); // BIsAppInstalled set(20, &user_steam_id); // GetAppOwner set(21, &empty_string); // GetLaunchQueryParam for (const auto slot : {12, 15, 24}) set(slot, &empty_string); // achievement/leaderboard names set(19, &empty_string); // GetFileNameAndSize set(2, &zero_steam_id); // GetSessionSteamID set(3, &empty_string); // GetSessionClientName set(12, &local_timestamp); // GetLocalTimestamp set(33, &one); // SetConfigValue const auto bind = [](int interface, std::array& vtable) { objects[interface].vtable = vtable.data(); }; bind(user, table); bind(friends, table); bind(utils, table); bind(matchmaking, table); bind(matchmaking_servers, table); bind(networking, table); bind(networking_messages, table); bind(networking_sockets, table); bind(networking_utils, table); bind(input, table); bind(controller, table); bind(parties, table); bind(game_search, table); bind(apps, table); bind(user_stats, table); bind(remote_storage, table); bind(remote_play, table); bind(unknown, table); } Interface classify(std::string_view version) noexcept { // Longest names first: prefixes overlap (SteamNetworking...). constexpr std::array, 17> names{{ {"STEAMAPPS_INTERFACE_VERSION", apps}, {"STEAMUSERSTATS_INTERFACE_VERSION", user_stats}, {"STEAMREMOTESTORAGE_INTERFACE_VERSION", remote_storage}, {"STEAMREMOTEPLAY_INTERFACE_VERSION", remote_play}, {"SteamMatchMakingServers", matchmaking_servers}, {"SteamMatchGameSearch", game_search}, {"SteamMatchMaking", matchmaking}, {"SteamNetworkingMessages", networking_messages}, {"SteamNetworkingSockets", networking_sockets}, {"SteamNetworkingUtils", networking_utils}, {"SteamNetworking", networking}, {"SteamUser", user}, {"SteamFriends", friends}, {"SteamUtils", utils}, {"SteamInput", input}, {"SteamController", controller}, {"SteamParties", parties}}}; for (const auto& [name, interface] : names) if (version.starts_with(name)) return interface; return unknown; } // ---------------------------------------------------------------- flat API void* __cdecl find_or_create_user_interface(std::int32_t, const char* version) { const auto interface = classify(version ? version : ""); if (interface == unknown) { try { logging::log(logging::Level::info, logging::Channel::network, "Offline Steam: generic stand-in for {}", version ? version : "(null)"); } catch (...) {} } return &objects[interface]; } void* __cdecl find_or_create_game_server_interface(std::int32_t, const char*) { return nullptr; } // Steamworks' per-interface accessor cache: {init function, counter, pointer}. void* __cdecl context_init(void* data) { auto* context = static_cast(data); if (!context) return nullptr; if (reinterpret_cast(context[1]) != 1) { reinterpret_cast(context[0])(&context[2]); context[1] = reinterpret_cast(std::uintptr_t{1}); } return &context[2]; } int __cdecl internal_init(const char*, char* error) { if (error) error[0] = '\0'; return 0; // k_ESteamAPIInitResult_OK } bool __cdecl init_ok() { return true; } void __cdecl shutdown() {} std::int32_t __cdecl handle() { return 1; } const char* __cdecl install_path() { return empty; } void __cdecl release_thread_memory() {} void detour(HMODULE module, const char* name, void* replacement) { const auto target = reinterpret_cast(GetProcAddress(module, name)); if (!target) return; const auto create = hook_prepare(target, replacement, nullptr); if (create != HookOk) throw std::runtime_error(std::string("Cannot hook ") + name + ": " + hook_status_string(create)); const auto enable = hook_enable(target); if (enable != HookOk) throw std::runtime_error(std::string("Cannot enable ") + name + ": " + hook_status_string(enable)); } } // namespace std::uint64_t steam_id() noexcept { return launcher::offline_steam_id(); } bool install(HMODULE steam, std::string& error) noexcept { try { build_objects(); #pragma warning(push) #pragma warning(disable: 4191) const std::pair replacements[]{ {"SteamInternal_SteamAPI_Init", reinterpret_cast(&internal_init)}, {"SteamAPI_Init", reinterpret_cast(&init_ok)}, {"SteamAPI_InitSafe", reinterpret_cast(&init_ok)}, {"SteamAPI_IsSteamRunning", reinterpret_cast(&init_ok)}, {"SteamAPI_Shutdown", reinterpret_cast(&shutdown)}, {"SteamAPI_GetHSteamUser", reinterpret_cast(&handle)}, {"SteamAPI_GetHSteamPipe", reinterpret_cast(&handle)}, {"SteamAPI_GetSteamInstallPath", reinterpret_cast(&install_path)}, {"SteamAPI_ReleaseCurrentThreadMemory", reinterpret_cast(&release_thread_memory)}, {"SteamAPI_RunCallbacks", reinterpret_cast(&run_callbacks)}, {"SteamAPI_RegisterCallback", reinterpret_cast(®ister_callback)}, {"SteamAPI_UnregisterCallback", reinterpret_cast(&unregister_callback)}, {"SteamAPI_RegisterCallResult", reinterpret_cast(®ister_call_result)}, {"SteamAPI_UnregisterCallResult", reinterpret_cast(&unregister_call_result)}, {"SteamInternal_ContextInit", reinterpret_cast(&context_init)}, {"SteamInternal_FindOrCreateUserInterface", reinterpret_cast(&find_or_create_user_interface)}, {"SteamInternal_FindOrCreateGameServerInterface", reinterpret_cast(&find_or_create_game_server_interface)}, }; #pragma warning(pop) for (const auto& [name, replacement] : replacements) detour(steam, name, replacement); error = "offline, SteamID " + std::to_string(steam_id()); return true; } catch (const std::exception& exception) { error = exception.what(); return false; } } } // namespace dingosdk::offline_steam