mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 18:37:07 +02:00
Mod archives (Engine/Vfs/mod_merge*) - A free archive index no longer has to sit at or below the highest index the layout already declares. The engine keeps archives in a table keyed by layer, install chunk and a 16-bit index, with no range to stay inside; what it cannot survive is a reference to an archive the layout never declared. Indices are taken lowest first, per package directory. - So a mod that collides takes an index of its own and its file is hard linked rather than concatenated, and no two mods share one archive's 4 GB of addressable bytes. Sharing is left for a mod added while the game runs, which has no declared index to take, and for a directory with no index left. - A placement addresses its payload with a 32-bit offset, so both append paths now refuse to cross 4 GB instead of wrapping round and pointing the game at some other payload: CasStore::write fails with the archive that is full, and the live-apply path checks archive 1's size plus the mod's before appending and asks for a restart, which gives the mod an archive of its own anyway. Injection through a hooked LoadLibraryW (Engine/Core/Platform) - A player could not launch at all: "Child LoadLibraryW entry bytes match neither Windows' kernel32 file nor this process". Anti-virus and overlays hook the entry with a jump to a per-process trampoline, so the bytes differ in the launcher and in the child and neither comparison could match. The guard already allowed the same hook when both processes carried it identically, so it refused on an accident of how the hook was written rather than on intent. - inline_jump_length recognises the forms a hooking engine writes (rel32 jmp or call, push/ret, jmp [rip], mov rax/jmp rax, short jmp). When one is present and every byte after it still matches the clean kernel32 file, the launch goes ahead: the body is Windows' own, and the provider image already matched on timestamp, checksum and size. A wholesale replacement is still refused, and now says so. - The failure names the modules loaded into Skate that came from neither Windows nor the game folder, instead of guessing at "security software". An accepted hook is logged as a warning with the same list. Launching (Launcher/launch.cpp) - Exporting loose scripts is a convenience, and a folder that would not take it was failing the whole launch: fs::create_directories threw and nothing caught it. The export is skipped, RESKATE_LOOSE_FILES goes to 0 so the runtime does not look for files that are not there, and Skate starts. Someone whose install is under Program Files can play; only editing loose scripts is lost. Failures worth reading (Launcher/problem.h) - A hooked LoadLibraryW, a refused write and ERROR_ELEVATION_REQUIRED each explain themselves now. Windows reports the last two in the user's own language -- "Zugriff verweigert", "La operación solicitada requiere elevación" -- so the rules key on the operation names and the error number, which are never translated. Saved skaters (Extension/Customization, Extension/Profile) - Outfits stopped saving and every slot showed the standard skater. The load hook marks a preset blocked on entry and clears it only on a full success, so a load refused for a catalog that was not ready yet left the preset blocked for the session and every later save was refused as previous_load_rejected. The game asks once per level load and does not ask again, so one lost race cost a skater until a restart. Logs show the catalog arriving three seconds after the refused load, at startup and again on a map switch 17 minutes later. - The record is remembered in CosmeticRuntime::pending_loads and applied by retry_pending_cosmetic_loads from the customization pump, which already waits for refresh_cosmetic_catalog. The preset stays blocked until a load really succeeds, so nothing is written over the outfit on disk. - Those records belong to the loadout manager's array, which can be replaced in between, so each pointer is checked against the live array exactly as a save checks one -- manager current, begin/end sane, 0x48 stride, at most 10 records, in range and aligned. A record that no longer belongs is dropped as retry_record_moved rather than written through.
122 lines
4.8 KiB
C++
122 lines
4.8 KiB
C++
#pragma once
|
|
#include "Extension/Profile/runtime_internal.h"
|
|
|
|
namespace dingosdk::profile_runtime {
|
|
struct CosmeticItemInfo {
|
|
std::uint32_t hash{};
|
|
std::vector<std::uint32_t> categories;
|
|
std::string category;
|
|
std::int32_t sort_priority{};
|
|
bool build_kit{};
|
|
};
|
|
|
|
struct CosmeticRuntime {
|
|
void (*initialize_starter)(bool, const void*, const void*){};
|
|
std::uintptr_t item_manager{};
|
|
std::uint32_t item_count{};
|
|
std::map<std::string, CosmeticItemInfo, std::less<>> items;
|
|
std::set<std::string> blocked_loadouts;
|
|
// Presets whose load was refused only because the cosmetics catalog was not
|
|
// ready yet, with the record the game asked us to fill. The game asks once
|
|
// per level load and does not ask again, so the pump retries these once the
|
|
// catalog is ready. Until one succeeds the preset stays in
|
|
// `blocked_loadouts`, so a save cannot write over the outfit on disk.
|
|
std::map<std::string, void*, std::less<>> pending_loads;
|
|
// Saved outfit items that were not installed when their preset loaded (a
|
|
// disabled or removed costume mod). The slot shows its default this session;
|
|
// saves keep the original item unless the player changes that slot.
|
|
struct HeldSlot { std::size_t recipe{}; profile::CosmeticSlot saved; std::string fallback; };
|
|
std::map<std::string, std::vector<HeldSlot>, std::less<>> held_slots;
|
|
std::set<std::string> diagnostics;
|
|
DWORD update_thread{};
|
|
// Bumped whenever `items` is rebuilt (a live mod apply changes what is
|
|
// installed); the published catalog is replaced when it falls behind.
|
|
std::uint64_t items_generation{};
|
|
std::uint64_t published_generation{};
|
|
std::uintptr_t published_manager{};
|
|
std::uintptr_t published_inventory{};
|
|
std::uintptr_t inventory_data_model{};
|
|
std::uint32_t published_inventory_count{};
|
|
bool catalog_failed{};
|
|
bool ownership_unavailable{};
|
|
std::int32_t observed_selection{-1};
|
|
};
|
|
|
|
CosmeticRuntime& cosmetic_runtime();
|
|
|
|
bool cosmetic_diagnostic(const char* operation, const char* reason, std::string_view id = {}) noexcept;
|
|
|
|
std::uint32_t cosmetic_hash(std::string_view value);
|
|
|
|
bool cosmetic_text(std::uintptr_t p, std::string& value);
|
|
|
|
bool cosmetic_array(std::uintptr_t p, std::size_t stride, std::size_t limit, std::uint32_t& count);
|
|
|
|
bool cosmetic_words(std::uintptr_t p, std::size_t limit, std::vector<std::uint32_t>& result);
|
|
|
|
bool refresh_cosmetic_catalog();
|
|
bool reserved_cosmetic(std::string_view asset);
|
|
bool reserved_cosmetic_hash(std::uint32_t hash);
|
|
|
|
struct CosmeticNativeItem {
|
|
const char* asset{};
|
|
const std::uint32_t* parameters{};
|
|
std::uint32_t hash{}, slot{};
|
|
};
|
|
|
|
struct CosmeticNativeRecipe {
|
|
std::uintptr_t resource{};
|
|
const std::uint32_t* scalars{};
|
|
const CosmeticNativeItem* items{};
|
|
const std::uint32_t* transient_mask{};
|
|
std::uint32_t transient_word{};
|
|
std::uint8_t transient_flag{};
|
|
std::array<std::byte, 3> padding{};
|
|
};
|
|
|
|
static_assert(sizeof(CosmeticNativeItem) == 0x18 && sizeof(CosmeticNativeRecipe) == 0x28);
|
|
|
|
bool read_cosmetic_recipes(const void* vector, profile::CosmeticLoadout& result,
|
|
std::vector<CosmeticNativeRecipe>& native);
|
|
|
|
template<class T> struct CosmeticBorrowedArray {
|
|
std::vector<std::uint64_t> storage;
|
|
explicit CosmeticBorrowedArray(std::size_t count) : storage((8 + count * sizeof(T) + 7) / 8) {
|
|
const auto size = static_cast<std::uint32_t>(count);
|
|
std::memcpy(storage.data(), &size, 4);
|
|
std::memcpy(reinterpret_cast<std::byte*>(storage.data()) + 4, &size, 4);
|
|
}
|
|
T* data() { return reinterpret_cast<T*>(reinterpret_cast<std::byte*>(storage.data()) + 8); }
|
|
};
|
|
|
|
bool cosmetic_slot_categories(std::uintptr_t resource, std::map<std::uint32_t, std::uint32_t>& result);
|
|
|
|
struct StarterTemplate {
|
|
const char* name;
|
|
std::uint32_t key, version;
|
|
};
|
|
|
|
inline constexpr std::array starter_templates{
|
|
StarterTemplate{"CASRecipeTemplate", 2759515148U, 2},
|
|
StarterTemplate{"SkateboardRecipeTemplate", 1583459055U, 1},
|
|
StarterTemplate{"MiscUserDataRecipeTemplate", 995494888U, 1},
|
|
};
|
|
|
|
bool complete_starter_recipes(const profile::CosmeticLoadout& value);
|
|
|
|
bool initialize_missing_starter(const std::vector<CosmeticNativeRecipe>& recipes);
|
|
|
|
void initialize_starter_from_expression(std::uintptr_t vm, std::uint32_t pc) noexcept;
|
|
|
|
bool recover_card_only_preset(std::string_view id, const profile::CosmeticLoadout& saved,
|
|
const profile::CosmeticLoadout& defaults);
|
|
|
|
bool load_cosmetic_hook(const void* wrapper, void* destination);
|
|
// Applies the outfits whose load was refused for a catalog that was not ready.
|
|
// Call from the customization pump, holding native_mutex, once
|
|
// refresh_cosmetic_catalog() is true.
|
|
void retry_pending_cosmetic_loads();
|
|
|
|
void save_cosmetic_hook(std::uintptr_t manager, void* record, const char* raw_id, const char* reason, bool changed_only);
|
|
}
|