exe icon from the player's own game files at runtime; close-clone prop jitter fix
This commit is contained in:
parent
4776a8d514
commit
b99dbc400d
5 changed files with 379 additions and 3 deletions
|
|
@ -234,6 +234,7 @@ set(SKATE3_COMMON_SOURCES
|
|||
src/skate3_shader_disasm.cpp
|
||||
src/skate3_title_update_installer.cpp
|
||||
src/skate3_user_settings.cpp
|
||||
src/skate3_win_icon.cpp
|
||||
third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp
|
||||
)
|
||||
if(APPLE)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "skate3_native_scene.h"
|
||||
#include "skate3_screenshot.h"
|
||||
#include "skate3_shader_disasm.h"
|
||||
#include "skate3_win_icon.h"
|
||||
#include "skate3_title_update_installer.h"
|
||||
#include "skate3_user_settings.h"
|
||||
|
||||
|
|
@ -687,6 +688,15 @@ std::optional<rex::PathConfig> Skate3BaseApp::OnFinalizePaths(
|
|||
}
|
||||
runtime_paths = std::move(tu_paths);
|
||||
}
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
// Window/taskbar + Explorer icon sourced from the user's OWN game art at
|
||||
// runtime (game/nxeart); the shipped exe and the repo carry no EA
|
||||
// artwork (see skate3_win_icon.h). Game files are guaranteed installed by
|
||||
// this point on Windows (the install wizards above run blocking).
|
||||
skate3::ApplyGameIconFromGameData(
|
||||
runtime_paths.game_data_root,
|
||||
window() ? window()->GetNativeWindowHandle() : nullptr);
|
||||
#endif
|
||||
return runtime_paths;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5332,7 +5332,8 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
|||
};
|
||||
uint64_t key = (uint64_t(item.mesh) << 8) | (k & 0xFF);
|
||||
DynHist* hp = &s_hist[key];
|
||||
if (hp->seen == s_frame || hist_dist2(*hp) > 2.25f) {
|
||||
const float own_d2 = hp->seen == s_frame ? 1e30f : hist_dist2(*hp);
|
||||
if (own_d2 > 1e-4f) {
|
||||
// The k-th slot mispairs: the game's sort lists RESHUFFLE same-mesh
|
||||
// clones as they and the camera move. For static props the
|
||||
// reset-on-jump guard below was enough (a mispair rendered raw for a
|
||||
|
|
@ -5341,7 +5342,24 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
|||
// stepped poses (the vehicle judder/catch-up). Re-pair by POSITION
|
||||
// instead: claim the unclaimed history of this mesh whose newest
|
||||
// pose is nearest, within the same 1.5 m one-tick jump gate.
|
||||
float best = 2.25f;
|
||||
// The search runs whenever the slot is not an (almost) EXACT
|
||||
// continuation, not only past the 1.5 m gate: clone placements
|
||||
// CLOSER than 1.5 m (the paired newspaper holders, 0.78 m apart)
|
||||
// otherwise inherit each other's history on every reshuffle, and the
|
||||
// interpolator renders them sliding between the two placements ("the
|
||||
// props jitter in and out of position"). Seeding `best` with the own
|
||||
// slot's distance keeps a claim strictly-nearer-only, so a genuinely
|
||||
// moving entity still prefers its own ring.
|
||||
// RIGID claims get a far tighter cap than the vehicles' 1.5 m: when a
|
||||
// close clone's OWN ring goes stale (its submission flapped for a few
|
||||
// frames), strictly-nearer alone still claims the TWIN's fresh ring
|
||||
// (own = 1e30) and slides once, the residual single jitter seen
|
||||
// after the strictly-nearer fix. No rigid prop moves 0.3 m in one sim
|
||||
// tick (18 m/s at 60 Hz), while clone placements sit >= 0.78 m apart;
|
||||
// a rigid item with no history inside 0.3 m starts a fresh ring and
|
||||
// renders raw at its correct placement instead.
|
||||
const float claim_cap = skinned ? 2.25f : 0.09f;
|
||||
float best = std::min(own_d2, claim_cap);
|
||||
DynHist* alt = nullptr;
|
||||
uint64_t alt_key = key;
|
||||
uint32_t fresh_k = 256; // first index past the dense key range
|
||||
|
|
@ -5596,7 +5614,23 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
|||
const float dz = item.world[14] - latest.w[14];
|
||||
d2 = dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
discontinuity = d2 > 2.25f; // > 1.5 m
|
||||
// Rigid uses the same tight one-tick bound as the claim cap
|
||||
// above: 0.78 m clone placements sit inside the vehicles' 1.5 m
|
||||
// gate, and lerping across a mispair IS the prop jitter.
|
||||
discontinuity = d2 > (skinned ? 2.25f : 0.09f);
|
||||
if (discontinuity && !skinned && d2 < 2.25f) {
|
||||
// A rigid step that only the tightened bound caught = a
|
||||
// close-clone mispair that would have LERPED (the prop jitter).
|
||||
static std::atomic<uint64_t> s_rigid_mispair{0};
|
||||
const uint64_t n =
|
||||
s_rigid_mispair.fetch_add(1, std::memory_order_relaxed);
|
||||
if (n < 24 || (n & 255u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: rigid close-clone mispair reset mesh={:08X} "
|
||||
"k={} d={:.2f}m (n={})",
|
||||
item.mesh, k, std::sqrt(d2), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (discontinuity) {
|
||||
h.count = 0;
|
||||
|
|
|
|||
307
src/skate3_win_icon.cpp
Normal file
307
src/skate3_win_icon.cpp
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
// Application icon from the user's own game files, see skate3_win_icon.h.
|
||||
|
||||
#include "skate3_win_icon.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <windows.h>
|
||||
// initguid must precede wincodec so the WIC CLSIDs/IIDs are defined in this
|
||||
// translation unit (no dependency on uuid.lib link order).
|
||||
#include <initguid.h>
|
||||
#include <wincodec.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <rex/logging.h>
|
||||
|
||||
namespace skate3 {
|
||||
namespace {
|
||||
|
||||
// First embedded PNG in the nxeart dashboard-art package = the 64x64 title
|
||||
// tile (two identical copies exist; either is fine).
|
||||
std::vector<uint8_t> ExtractFirstPng(const std::filesystem::path& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) {
|
||||
return {};
|
||||
}
|
||||
std::vector<uint8_t> data((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
static const uint8_t kMagic[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
|
||||
static const uint8_t kIend[4] = {'I', 'E', 'N', 'D'};
|
||||
for (size_t i = 0; i + 8 <= data.size(); ++i) {
|
||||
if (std::memcmp(data.data() + i, kMagic, 8) != 0) {
|
||||
continue;
|
||||
}
|
||||
for (size_t j = i + 8; j + 8 <= data.size(); ++j) {
|
||||
if (std::memcmp(data.data() + j, kIend, 4) == 0) {
|
||||
// IEND chunk data end + 4-byte CRC.
|
||||
return std::vector<uint8_t>(data.begin() + i, data.begin() + j + 8);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Decode the PNG and scale to `size` x `size` 32bpp BGRA (top-down rows).
|
||||
bool WicDecodeScaled(IWICImagingFactory* wic, const std::vector<uint8_t>& png,
|
||||
uint32_t size, std::vector<uint8_t>& bgra_out) {
|
||||
IWICStream* stream = nullptr;
|
||||
if (FAILED(wic->CreateStream(&stream))) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
IWICBitmapDecoder* decoder = nullptr;
|
||||
IWICBitmapFrameDecode* frame = nullptr;
|
||||
IWICBitmapScaler* scaler = nullptr;
|
||||
IWICFormatConverter* converter = nullptr;
|
||||
do {
|
||||
if (FAILED(stream->InitializeFromMemory(
|
||||
const_cast<BYTE*>(png.data()), DWORD(png.size())))) {
|
||||
break;
|
||||
}
|
||||
if (FAILED(wic->CreateDecoderFromStream(stream, nullptr,
|
||||
WICDecodeMetadataCacheOnDemand,
|
||||
&decoder))) {
|
||||
break;
|
||||
}
|
||||
if (FAILED(decoder->GetFrame(0, &frame))) {
|
||||
break;
|
||||
}
|
||||
if (FAILED(wic->CreateBitmapScaler(&scaler)) ||
|
||||
FAILED(scaler->Initialize(frame, size, size,
|
||||
WICBitmapInterpolationModeFant))) {
|
||||
break;
|
||||
}
|
||||
if (FAILED(wic->CreateFormatConverter(&converter)) ||
|
||||
FAILED(converter->Initialize(scaler, GUID_WICPixelFormat32bppBGRA,
|
||||
WICBitmapDitherTypeNone, nullptr, 0.0,
|
||||
WICBitmapPaletteTypeCustom))) {
|
||||
break;
|
||||
}
|
||||
bgra_out.resize(size_t(size) * size * 4);
|
||||
if (FAILED(converter->CopyPixels(nullptr, size * 4,
|
||||
UINT(bgra_out.size()), bgra_out.data()))) {
|
||||
break;
|
||||
}
|
||||
ok = true;
|
||||
} while (false);
|
||||
if (converter) converter->Release();
|
||||
if (scaler) scaler->Release();
|
||||
if (frame) frame->Release();
|
||||
if (decoder) decoder->Release();
|
||||
if (stream) stream->Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Icon-resource DIB image: BITMAPINFOHEADER with DOUBLED height, XOR (BGRA)
|
||||
// rows bottom-up, then an all-zero AND mask (alpha carries transparency).
|
||||
std::vector<uint8_t> BuildIconDib(uint32_t size,
|
||||
const std::vector<uint8_t>& bgra_topdown) {
|
||||
const uint32_t xor_bytes = size * size * 4;
|
||||
const uint32_t and_stride = ((size + 31) / 32) * 4;
|
||||
const uint32_t and_bytes = and_stride * size;
|
||||
std::vector<uint8_t> out(sizeof(BITMAPINFOHEADER) + xor_bytes + and_bytes, 0);
|
||||
auto* bih = reinterpret_cast<BITMAPINFOHEADER*>(out.data());
|
||||
bih->biSize = sizeof(BITMAPINFOHEADER);
|
||||
bih->biWidth = LONG(size);
|
||||
bih->biHeight = LONG(size) * 2; // XOR + AND, both bottom-up
|
||||
bih->biPlanes = 1;
|
||||
bih->biBitCount = 32;
|
||||
bih->biCompression = BI_RGB;
|
||||
bih->biSizeImage = xor_bytes + and_bytes;
|
||||
uint8_t* xor_dst = out.data() + sizeof(BITMAPINFOHEADER);
|
||||
for (uint32_t y = 0; y < size; ++y) {
|
||||
std::memcpy(xor_dst + size_t(y) * size * 4,
|
||||
bgra_topdown.data() + size_t(size - 1 - y) * size * 4,
|
||||
size_t(size) * 4);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#pragma pack(push, 2)
|
||||
struct GrpIconDirEntry {
|
||||
BYTE bWidth;
|
||||
BYTE bHeight;
|
||||
BYTE bColorCount;
|
||||
BYTE bReserved;
|
||||
WORD wPlanes;
|
||||
WORD wBitCount;
|
||||
DWORD dwBytesInRes;
|
||||
WORD nId;
|
||||
};
|
||||
struct GrpIconDir {
|
||||
WORD idReserved;
|
||||
WORD idType;
|
||||
WORD idCount;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// One-time self-patch: write RT_ICON entries + a MAINICON RT_GROUP_ICON into
|
||||
// the exe file. The running image is write-locked but RENAMABLE: patch a
|
||||
// copy, then swap it into place. The renamed original stays locked until
|
||||
// exit and is deleted by the NEXT (already-patched) run.
|
||||
void EnsureExeIconResource(const std::vector<uint8_t>& png) {
|
||||
wchar_t exe_w[MAX_PATH];
|
||||
if (GetModuleFileNameW(nullptr, exe_w, MAX_PATH) == 0) {
|
||||
return;
|
||||
}
|
||||
const std::filesystem::path exe(exe_w);
|
||||
const std::filesystem::path pre = exe.wstring() + L".pre-icon";
|
||||
if (FindResourceW(GetModuleHandleW(nullptr), L"MAINICON",
|
||||
reinterpret_cast<LPCWSTR>(RT_GROUP_ICON)) != nullptr) {
|
||||
// Already patched (or a build that ships an icon): just clean up any
|
||||
// leftover pre-patch original from the run that performed the swap.
|
||||
DeleteFileW(pre.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the multi-size images. WIC failure falls back to a single raw-PNG
|
||||
// entry (valid icon image format since Vista).
|
||||
struct IconImage {
|
||||
uint32_t size;
|
||||
std::vector<uint8_t> data;
|
||||
};
|
||||
std::vector<IconImage> images;
|
||||
const HRESULT co = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
{
|
||||
IWICImagingFactory* wic = nullptr;
|
||||
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&wic)))) {
|
||||
for (uint32_t s : {16u, 24u, 32u, 48u, 64u, 256u}) {
|
||||
std::vector<uint8_t> bgra;
|
||||
if (WicDecodeScaled(wic, png, s, bgra)) {
|
||||
images.push_back({s, BuildIconDib(s, bgra)});
|
||||
}
|
||||
}
|
||||
wic->Release();
|
||||
}
|
||||
}
|
||||
if (SUCCEEDED(co)) {
|
||||
CoUninitialize();
|
||||
}
|
||||
if (images.empty()) {
|
||||
images.push_back({64, png});
|
||||
}
|
||||
|
||||
GrpIconDir dir{0, 1, WORD(images.size())};
|
||||
std::vector<uint8_t> group(sizeof(GrpIconDir) +
|
||||
images.size() * sizeof(GrpIconDirEntry));
|
||||
std::memcpy(group.data(), &dir, sizeof(dir));
|
||||
for (size_t i = 0; i < images.size(); ++i) {
|
||||
GrpIconDirEntry e{};
|
||||
e.bWidth = BYTE(images[i].size >= 256 ? 0 : images[i].size);
|
||||
e.bHeight = e.bWidth;
|
||||
e.wPlanes = 1;
|
||||
e.wBitCount = 32;
|
||||
e.dwBytesInRes = DWORD(images[i].data.size());
|
||||
e.nId = WORD(i + 1);
|
||||
std::memcpy(group.data() + sizeof(GrpIconDir) + i * sizeof(e), &e,
|
||||
sizeof(e));
|
||||
}
|
||||
|
||||
// Patch a COPY first so a failure changes nothing.
|
||||
const std::filesystem::path temp = exe.wstring() + L".icon-new";
|
||||
if (!CopyFileW(exe.c_str(), temp.c_str(), FALSE)) {
|
||||
REXLOG_INFO("win-icon: exe copy for icon patch failed ({}); skipping",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
const WORD lang = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
|
||||
HANDLE update = BeginUpdateResourceW(temp.c_str(), FALSE);
|
||||
bool ok = update != nullptr;
|
||||
for (size_t i = 0; ok && i < images.size(); ++i) {
|
||||
ok = UpdateResourceW(update, reinterpret_cast<LPCWSTR>(RT_ICON),
|
||||
MAKEINTRESOURCEW(WORD(i + 1)), lang,
|
||||
images[i].data.data(), DWORD(images[i].data.size()));
|
||||
}
|
||||
if (ok) {
|
||||
ok = UpdateResourceW(update, reinterpret_cast<LPCWSTR>(RT_GROUP_ICON),
|
||||
L"MAINICON", lang, group.data(), DWORD(group.size()));
|
||||
}
|
||||
if (update != nullptr) {
|
||||
// Commit (discard on failure).
|
||||
ok = EndUpdateResourceW(update, !ok) && ok;
|
||||
}
|
||||
if (!ok) {
|
||||
REXLOG_INFO("win-icon: icon resource patch failed ({}); skipping",
|
||||
GetLastError());
|
||||
DeleteFileW(temp.c_str());
|
||||
return;
|
||||
}
|
||||
// Swap: rename the (locked but renamable) running exe aside, move the
|
||||
// patched copy into place. On the second step failing, restore.
|
||||
if (!MoveFileExW(exe.c_str(), pre.c_str(), MOVEFILE_REPLACE_EXISTING)) {
|
||||
REXLOG_INFO("win-icon: could not move running exe aside ({}); skipping",
|
||||
GetLastError());
|
||||
DeleteFileW(temp.c_str());
|
||||
return;
|
||||
}
|
||||
if (!MoveFileW(temp.c_str(), exe.c_str())) {
|
||||
MoveFileW(pre.c_str(), exe.c_str());
|
||||
return;
|
||||
}
|
||||
REXLOG_INFO(
|
||||
"win-icon: Explorer icon patched into {} from the game's own art "
|
||||
"(one-time; {} removed on next launch)",
|
||||
exe.filename().string(), pre.filename().string());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ApplyGameIconFromGameData(const std::filesystem::path& game_data_root,
|
||||
void* native_window_handle) {
|
||||
std::filesystem::path nxeart = game_data_root / "nxeart";
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(nxeart, ec)) {
|
||||
nxeart = game_data_root / "game" / "nxeart";
|
||||
}
|
||||
HWND hwnd = static_cast<HWND>(native_window_handle);
|
||||
std::thread([nxeart, hwnd] {
|
||||
const std::vector<uint8_t> png = ExtractFirstPng(nxeart);
|
||||
if (png.empty()) {
|
||||
REXLOG_INFO("win-icon: no title icon found in {}", nxeart.string());
|
||||
return;
|
||||
}
|
||||
if (hwnd != nullptr) {
|
||||
// CreateIconFromResourceEx accepts PNG image data (Vista+). Explicit
|
||||
// small size for crisp titlebar/taskbar small renditions.
|
||||
HICON icon_big = CreateIconFromResourceEx(
|
||||
const_cast<PBYTE>(png.data()), DWORD(png.size()), TRUE, 0x00030000,
|
||||
0, 0, LR_DEFAULTCOLOR);
|
||||
HICON icon_small = CreateIconFromResourceEx(
|
||||
const_cast<PBYTE>(png.data()), DWORD(png.size()), TRUE, 0x00030000,
|
||||
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
|
||||
LR_DEFAULTCOLOR);
|
||||
DWORD_PTR unused = 0;
|
||||
if (icon_big) {
|
||||
SendMessageTimeoutW(hwnd, WM_SETICON, ICON_BIG,
|
||||
reinterpret_cast<LPARAM>(icon_big), SMTO_NORMAL,
|
||||
2000, &unused);
|
||||
}
|
||||
if (icon_small) {
|
||||
SendMessageTimeoutW(hwnd, WM_SETICON, ICON_SMALL,
|
||||
reinterpret_cast<LPARAM>(icon_small), SMTO_NORMAL,
|
||||
2000, &unused);
|
||||
}
|
||||
// HICONs stay alive for the window's lifetime (never destroyed:
|
||||
// process-lifetime singletons).
|
||||
}
|
||||
EnsureExeIconResource(png);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace skate3
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
namespace skate3 {
|
||||
void ApplyGameIconFromGameData(const std::filesystem::path&, void*) {}
|
||||
} // namespace skate3
|
||||
|
||||
#endif
|
||||
24
src/skate3_win_icon.h
Normal file
24
src/skate3_win_icon.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace skate3 {
|
||||
|
||||
// Application icon sourced from the USER'S OWN game files at runtime; the
|
||||
// shipped executable and the repository deliberately contain no EA artwork
|
||||
// (same clean-distribution rule as every other game asset).
|
||||
//
|
||||
// Windows only (no-op elsewhere). Two halves, both fed by the 64x64 title
|
||||
// tile PNG embedded in game/nxeart (the Xbox dashboard-art package):
|
||||
// - window/taskbar/Alt-Tab icon: WM_SETICON on the game window, every run;
|
||||
// - Explorer file icon: a one-time self-patch that writes a MAINICON
|
||||
// resource group into the exe ON DISK (the running image is rename-and-
|
||||
// copy swapped since it is write-locked). Idempotent: subsequent runs see
|
||||
// the resource and skip; overwriting the exe with an update simply
|
||||
// re-patches on the next run.
|
||||
//
|
||||
// Runs on a background thread; failures only log (the icon stays default).
|
||||
void ApplyGameIconFromGameData(const std::filesystem::path& game_data_root,
|
||||
void* native_window_handle);
|
||||
|
||||
} // namespace skate3
|
||||
Loading…
Add table
Reference in a new issue