Files
ReSkate/Extension/Scripting/lua_native_block.cpp
T
AyeZeeBB 86aa8d38fb 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.
2026-10-12 16:03:35 -05:00

127 lines
5.8 KiB
C++

#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, &nothing<0>}, {lua::io_popen, &nothing<1>}, {lua::io_open, &nothing<2>},
{lua::io_output, &nothing<3>}, {lua::os_remove, &nothing<4>}, {lua::os_rename, &nothing<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