mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 06:05:15 +02:00
Music: local files, online radio, equaliser, queue, and a Music page in the menu
Sources - Local Files: add a folder of music from the setup page (MP3, M4A, AAC, Ogg, FLAC, WAV; up to 200 songs a folder, 12 folders). Names and covers come from the files' tags. Each folder is a tile in its own row. - Online Radio: find stations by name (radio-browser.info) or add a stream's address (MP3, AAC, .pls, .m3u; no HLS). Each station is a tile in its own row; the track name follows what the station sends. Up to 24. - Both are decoded by the library built into the DLL (SpotifyHelper), which now plays files and HTTP streams as well as a program's output, reads tags and embedded pictures, and accepts MPEG-2 ADTS from AAC stations. Sound - Five-band equaliser (60 Hz, 250 Hz, 1 kHz, 4 kHz, 12 kHz) and loudness levelling, applied to every source on its way into the game. - Loudness, the speakers option, the equaliser and levelling are now kept in music-settings.json between sessions. Setup page - Remade in the mod's own colours and type, with numbered steps per source. - Queue pinned to the right: now playing, the playlist's songs with covers, find, and press to jump. Spotify's own queue shows once signed in. - Cards for Local files and Online radio; equaliser and levelling under Sound. In game - MUSIC page in the menu: player and queue, sound settings, source status and a button for the setup page. - "Loading song..." notice in the corner for songs that take a moment, with a switch to hide it. Presses made while a song is loading no longer restart it. - The Spotify row is hidden when Spotify is not in use. - Console: "spotify ..." is now "music ..." (music, music setup, music gain, music through, music spotify client|login|logout). Logs - The music sources' working lines (traces, per-song and per-bind lines, librespot's own) are debug only; stack captures are skipped unless debug logging is on. Grind exits: the per-pop line is debug only too. - A "Pace:" line when the game takes the sound faster or slower than real time, to find why music sounds fast after tabbing back in. Chat filter - A run of digits is a number, not a word spelled with look-alikes: a race time such as 2:55.0 is no longer masked.
This commit is contained in:
1 parent
ea17559e3d
commit
86aa8d38fb
39 files changed
+2565
-321
No files matched your search
@@ -0,0 +1,126 @@
|
||||
#include "lua_native_block.h"
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/lua_natives.h"
|
||||
#include <Windows.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
|
||||
namespace dingosdk {
|
||||
namespace {
|
||||
namespace lua = addr::lua_natives;
|
||||
using LoadLibraryFn = int (*)(void* state, const char* path, const char* symbol);
|
||||
using PushFormat = const char* (*)(void* state, const char* format, ...);
|
||||
using ParseChunk = void (*)(void* state, void* parser);
|
||||
using LuaFunction = int (*)(void* state);
|
||||
|
||||
std::mutex install_mutex;
|
||||
std::uintptr_t installed_base{};
|
||||
PushFormat push_format{};
|
||||
std::atomic<ParseChunk> original_parse{};
|
||||
|
||||
// A script that keeps trying must not fill the log.
|
||||
void refused(std::atomic<unsigned>& count, std::string_view what, std::string_view detail = {}) noexcept {
|
||||
if (count.fetch_add(1, std::memory_order_relaxed) >= 8) return;
|
||||
try {
|
||||
logging::log(logging::Level::warning, logging::Channel::assets, "Lua script refused: {}{}{}. ReSkate does not let Lua do this.",
|
||||
what, detail.empty() ? "" : " ", detail);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
// A path a script made up, for the log: one line, printable, not too long.
|
||||
std::string shown(const char* text) noexcept {
|
||||
std::array<char, 200> copy{};
|
||||
if (!text || memory::peek_cstring(reinterpret_cast<std::uintptr_t>(text), copy.data(), copy.size()) < 0) return {};
|
||||
std::string result(copy.data());
|
||||
for (auto& c : result) if (static_cast<unsigned char>(c) < 32 || static_cast<unsigned char>(c) > 126) c = '?';
|
||||
return result;
|
||||
}
|
||||
|
||||
// The DLL "did not load": package.loadlib answers nil and this message, require raises it.
|
||||
int load_library(void* state, const char* path, const char*) {
|
||||
static std::atomic<unsigned> count{};
|
||||
refused(count, "loading a DLL", shown(path));
|
||||
push_format(state, "ReSkate does not let Lua load a DLL");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Text only. A chunk that starts with the bytecode signature fails Lua's own check
|
||||
// ("attempt to load a binary chunk (mode is 't')").
|
||||
void parse_chunk(void* state, void* parser) {
|
||||
if (parser) *reinterpret_cast<const char**>(static_cast<unsigned char*>(parser) + lua::parser_mode_offset) = "t";
|
||||
original_parse.load(std::memory_order_acquire)(state, parser);
|
||||
}
|
||||
|
||||
// The rest answer nothing at all, which a script reads as nil.
|
||||
struct Refusal { const game::build::Fingerprint& contract; LuaFunction replacement; };
|
||||
constexpr std::size_t refusal_count = 6;
|
||||
constexpr std::array<std::string_view, refusal_count> refusal_names{
|
||||
"os.execute (starting a program)", "io.popen (starting a program)", "io.open (opening a file on disk)",
|
||||
"io.output (opening a file on disk)", "os.remove (deleting a file)", "os.rename (renaming a file)"};
|
||||
template<std::size_t Index> int nothing(void*) {
|
||||
static std::atomic<unsigned> count{};
|
||||
refused(count, refusal_names[Index]);
|
||||
return 0;
|
||||
}
|
||||
const std::array<Refusal, refusal_count> refusals{{
|
||||
{lua::os_execute, ¬hing<0>}, {lua::io_popen, ¬hing<1>}, {lua::io_open, ¬hing<2>},
|
||||
{lua::io_output, ¬hing<3>}, {lua::os_remove, ¬hing<4>}, {lua::os_rename, ¬hing<5>}}};
|
||||
|
||||
bool matches(std::uintptr_t base, const game::build::Fingerprint& contract) noexcept {
|
||||
std::array<unsigned char, 32> bytes{};
|
||||
return memory::read_bytes(base + contract.rva, bytes.data(), bytes.size()) && bytes == contract.bytes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool start_lua_native_block(std::uintptr_t base, std::string& error) {
|
||||
std::lock_guard lock(install_mutex);
|
||||
error.clear();
|
||||
if (installed_base) {
|
||||
if (installed_base == base) return true;
|
||||
error = "Lua's native functions are already switched off on another image";
|
||||
return false;
|
||||
}
|
||||
if (!base || !matches(base, lua::load_library) || !matches(base, lua::push_format) || !matches(base, lua::parse_chunk)) {
|
||||
error = "Native Lua contract does not match the supported game"; return false;
|
||||
}
|
||||
for (const auto& refusal : refusals)
|
||||
if (!matches(base, refusal.contract)) { error = "Native Lua library contract does not match the supported game"; return false; }
|
||||
push_format = reinterpret_cast<PushFormat>(base + lua::push_format.rva);
|
||||
|
||||
std::array<void*, refusal_count + 2> targets{};
|
||||
std::size_t prepared = 0;
|
||||
const auto undo = [&] { for (std::size_t i = 0; i < prepared; ++i) hook_remove(targets[i]); };
|
||||
const auto prepare = [&](std::uintptr_t rva, void* replacement, void** original) {
|
||||
auto* target = reinterpret_cast<void*>(base + rva);
|
||||
const auto status = hook_prepare(target, replacement, original);
|
||||
if (status != HookOk) { error = "Cannot prepare the Lua native block: " + std::to_string(status); return false; }
|
||||
targets[prepared++] = target;
|
||||
return true;
|
||||
};
|
||||
ParseChunk parse{};
|
||||
bool ready = prepare(lua::load_library.rva, reinterpret_cast<void*>(&load_library), nullptr) &&
|
||||
prepare(lua::parse_chunk.rva, reinterpret_cast<void*>(&parse_chunk), reinterpret_cast<void**>(&parse)) && parse;
|
||||
for (std::size_t i = 0; ready && i < refusals.size(); ++i)
|
||||
ready = prepare(refusals[i].contract.rva, reinterpret_cast<void*>(refusals[i].replacement), nullptr);
|
||||
if (!ready) {
|
||||
if (error.empty()) error = "Cannot prepare the Lua native block";
|
||||
undo(); return false;
|
||||
}
|
||||
original_parse.store(parse, std::memory_order_release);
|
||||
auto status = HookOk;
|
||||
for (std::size_t i = 0; status == HookOk && i < prepared; ++i) status = hook_queue_enable(targets[i]);
|
||||
if (status == HookOk) status = hook_apply_queued();
|
||||
if (status != HookOk) {
|
||||
undo();
|
||||
original_parse.store(nullptr, std::memory_order_release);
|
||||
error = "Cannot switch off Lua's native functions: " + std::to_string(status); return false;
|
||||
}
|
||||
installed_base = base;
|
||||
return true;
|
||||
}
|
||||
} // namespace dingosdk
|
||||
Reference in new issue
Block a user