mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-08-22 18:30:32 +00:00
delete SlippiRustExtensions locally
This commit is contained in:
parent
237b802930
commit
4682cfcbe2
34 changed files with 0 additions and 5385 deletions
|
@ -1,5 +0,0 @@
|
||||||
# We want this set on all builds in order to keep unwind tables for backtraces
|
|
||||||
# included in the final build. It does bloat things, but it's worth it for us
|
|
||||||
# in order to get more accurate crash logs from users.
|
|
||||||
[build]
|
|
||||||
rustflags = ["-Cforce-unwind-tables=y"]
|
|
1812
Externals/SlippiRustExtensions/Cargo.lock
generated
vendored
1812
Externals/SlippiRustExtensions/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
35
Externals/SlippiRustExtensions/Cargo.toml
vendored
35
Externals/SlippiRustExtensions/Cargo.toml
vendored
|
@ -1,35 +0,0 @@
|
||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
|
|
||||||
# Always build the ffi interface as the default project.
|
|
||||||
default-members = ["ffi"]
|
|
||||||
|
|
||||||
members = [
|
|
||||||
"dolphin",
|
|
||||||
"exi",
|
|
||||||
"ffi",
|
|
||||||
"game-reporter",
|
|
||||||
"jukebox",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Ship the resulting dylib with debug information so we can figure out
|
|
||||||
# crashes easier, and swap panic behavior to "abort" to match C++ behavior.
|
|
||||||
#
|
|
||||||
# We don't want to panic and attempt to unwind into C/C++ since that can cause
|
|
||||||
# undefined behavior.
|
|
||||||
[profile.release]
|
|
||||||
debug = true
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
fastrand = "2.0.0"
|
|
||||||
time = { version = "0.3.20", default-features = false, features = ["std", "local-offset"] }
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = { version = "1" }
|
|
||||||
serde_repr = { version = "0.1" }
|
|
||||||
|
|
||||||
# We disable the "attributes" feature as we don't currently need it and it brings
|
|
||||||
# in extra dependencies.
|
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
ureq = { version = "2.7.1", features = ["json"] }
|
|
74
Externals/SlippiRustExtensions/README.md
vendored
74
Externals/SlippiRustExtensions/README.md
vendored
|
@ -1,74 +0,0 @@
|
||||||
# Slippi Rust Extensions
|
|
||||||
This external module houses various Slippi-specific bits of functionality and is ultimately linked into the Dolphin executable and instrumented via the C FFI. This is an evolving workspace area and may be subject to changes.
|
|
||||||
|
|
||||||
This workspace currently targets Rust `1.70.0`. As long as you have Rust installed, `cargo` should automatically configure this when building.
|
|
||||||
|
|
||||||
> You may opt to add other components (e.g `clippy`) as the toolchain file currently targets what the CI builders need.
|
|
||||||
|
|
||||||
- [Project Module Structure Overview](#project-module-structure-overview)
|
|
||||||
- [Adding a new workspace module](#adding-a-new-workspace-module)
|
|
||||||
- [Feature Flags](#feature-flags)
|
|
||||||
- [Building out of band](#building-out-of-band)
|
|
||||||
|
|
||||||
## Project Module Structure Overview
|
|
||||||
|
|
||||||
| Module | Description |
|
|
||||||
|------------------------|----------------------------------------------------------------------------|
|
|
||||||
| `dolphin-integrations` | A library that wraps Dolphin callbacks (logging, etc). |
|
|
||||||
| `exi` | EXI device that receives forwarded calls from the EXI (C++) device. |
|
|
||||||
| `ffi` | The core library. Exposes C FFI functions for Dolphin to call. |
|
|
||||||
| `game-reporter` | Implements match and event reporting. |
|
|
||||||
| `jukebox` | Melee music player library. See the [Slippi Jukebox README](jukebox/README.md) for more info. |
|
|
||||||
|
|
||||||
Some important aspects of the project structure to understand:
|
|
||||||
|
|
||||||
- The build script in this crate automatically generates C bindings that get output to `ffi/includes/SlippiRustExtensions.h`, and the Dolphin CMake and Visual Studio projects are pre-configured to find this header and link the necessary libraries and dlls.
|
|
||||||
- The **entry point** of this is the `ffi` crate. (e.g, `Dolphin (C++) -> ffi (C) -> (EXI) (Rust)`)
|
|
||||||
- As Rust is not the _host_ language, it cannot import anything from the C++ side of things. This means that when you go to bridge things, you'll need to thread calls and data through from the C++ side of things to the Rust side of things. In practice, this shouldn't be too bad - much of the Slippi-specific logic is contained within the `EXI_DeviceSlippi.cpp` class.
|
|
||||||
- The aforementioned `EXI_DeviceSlippi.cpp` class *owns* the Rust EXI device. When the EXI device is created, the Rust EXI device is created. When the EXI device is torn down, the Rust device is torn down. Do not alter this lifecycle pattern; it keeps things predictable regarding ownership and fits well with the Rust approach.
|
|
||||||
- If you add a new FFI function, please prefix it with `slprs_`. This helps keep the Dolphin codebase well segmented for "Slippi-Rust"-specific functionality.
|
|
||||||
|
|
||||||
> Be careful when trying to pass C++ types! _C_ has a stable ABI, _C++_ does not - you **always go through C**.
|
|
||||||
|
|
||||||
## Adding a new workspace module
|
|
||||||
If you're adding a new workspace module, simply create it (`cargo new --lib my_module`), add it to the root `Cargo.toml`, and setup/link it/call it elsewhere accordingly.
|
|
||||||
|
|
||||||
If this is code that Dolphin needs to call (via the C FFI), then it belongs in the `ffi` module. Your exposed method in the `ffi` module can/should forward on to wherever your code actually lives.
|
|
||||||
|
|
||||||
## Feature Flags
|
|
||||||
|
|
||||||
### The `ishiiruka/mainline` feature(s)
|
|
||||||
These two features control which codebase this extension is built against: Ishiiruka (the older build) or Dolphin (mainline Dolphin, much newer). `ishiiruka` is the current default feature until further notice.
|
|
||||||
|
|
||||||
### The `playback` feature
|
|
||||||
There is an optional feature flag for this repository for playback-specific functionality. This will automatically be set via either CMake or Visual Studio if you're building a Playback-enabled project, but if you're building and testing out of band you may need to enable this flag, e.g:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build --release --features playback
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building out of band
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
To compile changes made to the Rust extensions without having to rebuild the entire Dolphin project, you can compile _just_ the Rust codebase in the target area. This can work as the Rust code is linked to the Dolphin side of things at runtime; if you alter the Dolphin codebase, _or if you alter the exposed C FFI_, you will need a full project rebuild.
|
|
||||||
|
|
||||||
In the `SlippiRustExtensions` directory, run the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build --release --target=x86_64-pc-windows-msvc
|
|
||||||
```
|
|
||||||
|
|
||||||
> This is necessary on Windows to allow for situations where developers may need to run a VM to test across OS instances. The default `release` path can conflict on the two when mounted on the same filesystem, and we need the Visual Studio builder to know where to reliably look.
|
|
||||||
|
|
||||||
#### macOS/Linux/etc
|
|
||||||
Simply rebuild with the release flag.
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Formatting
|
|
||||||
The following line will format the entire project
|
|
||||||
```
|
|
||||||
cargo fmt --all --manifest-path=./Cargo.toml
|
|
||||||
```
|
|
|
@ -1,23 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "dolphin-integrations"
|
|
||||||
description = "Shims for calling back into C++ for Dolphin-specific functionality."
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = [
|
|
||||||
"Slippi Team",
|
|
||||||
"Ryan McGrath <ryan@rymc.io>"
|
|
||||||
]
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
ishiiruka = []
|
|
||||||
mainline = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
time = { workspace = true }
|
|
||||||
|
|
||||||
# We disable the "attributes" feature as we don't currently need it and it brings
|
|
||||||
# in extra dependencies.
|
|
||||||
tracing = { workspace = true }
|
|
||||||
tracing-subscriber = "0.3"
|
|
51
Externals/SlippiRustExtensions/dolphin/README.md
vendored
51
Externals/SlippiRustExtensions/dolphin/README.md
vendored
|
@ -1,51 +0,0 @@
|
||||||
# Dolphin Integrations
|
|
||||||
This crate implements various hooks and functionality for interacting with Dolphin from the Rust side. Notably, it contains a custom [tracing-subscriber](https://crates.io/crates/tracing-subscriber) that handles shuttling logs through the Dolphin application logging infrastructure. This is important not just for keeping logs contained all in one place, but for aiding in debugging on Windows where console logging is... odd.
|
|
||||||
|
|
||||||
## How Logging works
|
|
||||||
The first thing to understand is that the Slippi Dolphin `LogManager` module has some tweaks to support creating a "Rust-sourced" `LogContainer`.
|
|
||||||
|
|
||||||
When the `LogManager` is created, we initialize the Rust logging framework by calling `slprs_logging_init`, passing a function to dispatch logs through from the Rust side.
|
|
||||||
|
|
||||||
The `LogManager` has several `LogContainer` instances, generally corresponding to a particular grouping of log types. If a `LogContainer` is flagged as sourcing from the Rust library, then on instantiation it will register itself with the Rust logger by calling `slprs_logging_register_container`. This registration call caches the `LogType` and enabled status of the log so that the Rust side can transparently keep track of things and not have to concern itself with any changing enum variant types.
|
|
||||||
|
|
||||||
When a Rust-sourced `LogContainer` is updated - say, a status change to `disabled` - it will forward this change to the Rust side via `slprs_logging_update_container`. If a log container is disabled in Rust, then logs are dropped accordingly with no allocations made anywhere (i.e, nothing should be impacting gameplay).
|
|
||||||
|
|
||||||
## Adding a new LogContainer
|
|
||||||
If you need to add a new log to the Dolphin codebase, you will need to add a few lines to the log definitions on the C++ side. This enables using the Dolphin logs viewer to monitor Rust `tracing` events.
|
|
||||||
|
|
||||||
First, head to [`Source/Core/Common/Logging/Log.h`](../../../Source/Core/Common/Logging/Log.h) and define a new `LogTypes::LOG_TYPE` variant.
|
|
||||||
|
|
||||||
Next, head to [`Source/Core/Common/Logging/LogManager.cpp`](../../../Source/Core/Common/Logging/LogManager.cpp) and add a new `LogContainer` in `LogManager::LogManager()`. For example, let's say that we created `LogTypes::SLIPPI_RUST_EXI` - we'd now add:
|
|
||||||
|
|
||||||
``` c++
|
|
||||||
// This LogContainer will register with the Rust side under the "SLIPPI_RUST_EXI" target.
|
|
||||||
m_Log[LogTypes::SLIPPI_RUST_EXI] = new LogContainer(
|
|
||||||
"SLIPPI_RUST_EXI", // Internal identifier, Rust will need to match
|
|
||||||
"Slippi EXI (Rust)", // User-visible log label
|
|
||||||
LogTypes::SLIPPI_RUST_EXI, // The C++ LogTypes variant we created
|
|
||||||
true // Instructs the initializer that this is a Rust-sourced log
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, add an associated `const` declaration with your `LogContainer` internal identifier in `src/lib.rs`:
|
|
||||||
|
|
||||||
``` rust
|
|
||||||
pub mod Log {
|
|
||||||
// ...other logs etc
|
|
||||||
|
|
||||||
// Our new logger name
|
|
||||||
pub const EXI: &'static str = "SLIPPI_RUST_EXI";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now your Rust module can specify that it should log to this container via the tracing module:
|
|
||||||
|
|
||||||
``` rust
|
|
||||||
use dolphin_integrations::Log;
|
|
||||||
|
|
||||||
fn do_stuff() {
|
|
||||||
tracing::info!(target: Log::EXI, "Hello from the Rust side");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to _enable_ your log via Dolphin's Log Manager view to see these logs!
|
|
|
@ -1,71 +0,0 @@
|
||||||
//! This module implements various shims for calling back into C++ (the Dolphin side of things).
|
|
||||||
//!
|
|
||||||
//! Typically you can just import the `Dolphin` struct and use that as a namespace for accessing
|
|
||||||
//! all functionality at once.
|
|
||||||
|
|
||||||
use std::ffi::{c_char, CString};
|
|
||||||
|
|
||||||
mod logger;
|
|
||||||
pub use logger::Log;
|
|
||||||
|
|
||||||
mod osd;
|
|
||||||
pub use osd::{Color, Duration};
|
|
||||||
|
|
||||||
/// These types are primarily used by the `ffi` crate to wire up Dolphin functionality,
|
|
||||||
/// and you shouldn't need to touch them yourself. We're just re-exporting these under
|
|
||||||
/// a different namespace to avoid cluttering up autocomplete and confusing anyone.
|
|
||||||
pub mod ffi {
|
|
||||||
pub mod osd {
|
|
||||||
pub use crate::osd::set_global_hook;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod logger {
|
|
||||||
pub use crate::logger::{init, register_container, update_container, mainline_update_log_level};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// At the moment, this struct holds nothing and is simply a namespace to make importing and
|
|
||||||
/// accessing functionality less verbose.
|
|
||||||
///
|
|
||||||
/// tl;dr it's a namespace.
|
|
||||||
pub struct Dolphin;
|
|
||||||
|
|
||||||
impl Dolphin {
|
|
||||||
/// Renders a message in the current game window via the On-Screen-Display.
|
|
||||||
///
|
|
||||||
/// This function accepts anything that can be allocated into a `String` (which
|
|
||||||
/// includes just passing a `String` itself).
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use dolphin_integrations::{Color, Dolphin, Duration};
|
|
||||||
///
|
|
||||||
/// fn stuff() {
|
|
||||||
/// Dolphin::add_osd_message(Color::Cyan, Duration::Short, "Hello there");
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn add_osd_message<S>(color: Color, duration: Duration, message: S)
|
|
||||||
where
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
let message: String = message.into();
|
|
||||||
|
|
||||||
if let Some(hook_fn) = osd::MESSAGE_HOOK.get() {
|
|
||||||
let c_str_msg = match CString::new(message) {
|
|
||||||
Ok(c_string) => c_string,
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(?error, "Unable to allocate OSD String, not rendering");
|
|
||||||
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let color = color.to_u32();
|
|
||||||
let duration = duration.to_u32();
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
hook_fn(c_str_msg.as_ptr() as *const c_char, color, duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
//! This module implements a custom `tracing_subscriber::Layer` that facilitates
|
|
||||||
//! routing logs through the Dolphin log viewer.
|
|
||||||
|
|
||||||
use std::ffi::CString;
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::os::raw::{c_char, c_int};
|
|
||||||
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use tracing::{Level, Metadata};
|
|
||||||
use tracing_subscriber::Layer;
|
|
||||||
|
|
||||||
use super::{ForeignLoggerFn, Log, LOG_CONTAINERS};
|
|
||||||
|
|
||||||
/// Corresponds to Dolphin's `LogTypes::LOG_LEVELS::LNOTICE` value.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const LOG_LEVEL_NOTICE: c_int = 1;
|
|
||||||
|
|
||||||
/// Corresponds to Dolphin's `LogTypes::LOG_LEVELS::LERROR` value.
|
|
||||||
const LOG_LEVEL_ERROR: c_int = 2;
|
|
||||||
|
|
||||||
/// Corresponds to Dolphin's `LogTypes::LOG_LEVELS::LWARNING` value.
|
|
||||||
const LOG_LEVEL_WARNING: c_int = 3;
|
|
||||||
|
|
||||||
/// Corresponds to Dolphin's `LogTypes::LOG_LEVELS::LINFO` value.
|
|
||||||
const LOG_LEVEL_INFO: c_int = 4;
|
|
||||||
|
|
||||||
/// Corresponds to Dolphin's `LogTypes::LOG_LEVELS::LDEBUG` value.
|
|
||||||
const LOG_LEVEL_DEBUG: c_int = 5;
|
|
||||||
|
|
||||||
/// A helper method for converting Dolphin's levels to a tracing::Level.
|
|
||||||
///
|
|
||||||
/// Currently there's a bit of a mismatch, as `NOTICE` from Dolphin isn't
|
|
||||||
/// really covered here...
|
|
||||||
pub fn convert_dolphin_log_level_to_tracing_level(level: c_int) -> Level {
|
|
||||||
match level {
|
|
||||||
LOG_LEVEL_ERROR => Level::ERROR,
|
|
||||||
LOG_LEVEL_WARNING => Level::WARN,
|
|
||||||
LOG_LEVEL_INFO => Level::INFO,
|
|
||||||
_ => Level::DEBUG,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A custom tracing layer that forwards events back into the Dolphin logging infrastructure.
|
|
||||||
///
|
|
||||||
/// This implements `tracing_subscriber::Layer` and is the default way to log in this library.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DolphinLoggerLayer {
|
|
||||||
logger_fn: ForeignLoggerFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DolphinLoggerLayer {
|
|
||||||
/// Creates and returns a new logger layer.
|
|
||||||
pub fn new(logger_fn: ForeignLoggerFn) -> Self {
|
|
||||||
Self { logger_fn }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Layer<S> for DolphinLoggerLayer
|
|
||||||
where
|
|
||||||
S: tracing::Subscriber,
|
|
||||||
{
|
|
||||||
/// Unpacks a tracing event and routes it to the appropriate Dolphin log handler.
|
|
||||||
///
|
|
||||||
/// At the moment, this is somewhat "dumb" and may allocate more than we want to. Consider
|
|
||||||
/// it a hook for safely improving performance later. ;P
|
|
||||||
fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
|
|
||||||
let metadata = event.metadata();
|
|
||||||
let target = metadata.target();
|
|
||||||
|
|
||||||
let log_containers = LOG_CONTAINERS
|
|
||||||
.get()
|
|
||||||
.expect("[DolphinLoggerLayer::on_event]: Unable to acquire `LOG_CONTAINERS`?");
|
|
||||||
|
|
||||||
let reader = log_containers
|
|
||||||
.read()
|
|
||||||
.expect("[DolphinLoggerLayer::on_event]: Unable to acquire readlock on `LOG_CONTAINERS`?");
|
|
||||||
|
|
||||||
let mut log_container = reader.iter().find(|container| container.kind == target);
|
|
||||||
|
|
||||||
// If we can't find the requested container, then go ahead and see if we can dump it
|
|
||||||
// into the dependencies container. This also allows us to grab any logs that *aren't*
|
|
||||||
// tagged that may be of interest (i.e, from dependencies - hence the container name).
|
|
||||||
if log_container.is_none() {
|
|
||||||
log_container = reader.iter().find(|container| container.kind == Log::DEPENDENCIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
if log_container.is_none() {
|
|
||||||
// We want to still dump errors to the console if no log handler is set at all,
|
|
||||||
// otherwise debugging is a nightmare (i.e, we want to surface event flow if a
|
|
||||||
// logger initialization is mis-called somewhere).
|
|
||||||
eprintln!("No logger handler found for target: {}", target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = log_container.unwrap();
|
|
||||||
let level = *metadata.level();
|
|
||||||
|
|
||||||
// In tracing, ERROR is the *lowest* - so we essentially just want to make sure that we're
|
|
||||||
// at a level *above* the container level before allocating a log message.
|
|
||||||
if !container.is_enabled {
|
|
||||||
// @TODO: We want to avoid any allocations if the log level
|
|
||||||
// is not appropriate, but there's a mismatch between how Dolphin prioritizes log
|
|
||||||
// levels and how tracing does it.
|
|
||||||
//
|
|
||||||
// || container.level > level {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let log_level = match level {
|
|
||||||
Level::INFO => LOG_LEVEL_INFO,
|
|
||||||
Level::WARN => LOG_LEVEL_WARNING,
|
|
||||||
Level::ERROR => LOG_LEVEL_ERROR,
|
|
||||||
Level::DEBUG | Level::TRACE => LOG_LEVEL_DEBUG,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut visitor = DolphinLoggerVisitor::new(metadata, target);
|
|
||||||
event.record(&mut visitor);
|
|
||||||
|
|
||||||
match CString::new(visitor.finish()) {
|
|
||||||
Ok(c_str_msg) => {
|
|
||||||
// A note on ownership: the Dolphin logger cannot modify the contents of
|
|
||||||
// the passed over c_str, nor should it attempt to free anything. Rust owns
|
|
||||||
// this and will handle any cleanup after the logger_fn has dispatched.
|
|
||||||
unsafe {
|
|
||||||
(self.logger_fn)(log_level, container.log_type, c_str_msg.as_ptr() as *const c_char);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// This should never happen, but on the off chance it does, I guess
|
|
||||||
// just dump it to stderr?
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to convert info msg to CString: {:?}", e);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implements a visitor that builds a log message for the logger functionality in Dolphin
|
|
||||||
/// to consume. This currently builds a String internally that can be passed over to
|
|
||||||
/// the Dolphin side.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct DolphinLoggerVisitor(String);
|
|
||||||
|
|
||||||
impl DolphinLoggerVisitor {
|
|
||||||
/// Creates and returns a new `DolphinLoggerVisitor`.
|
|
||||||
///
|
|
||||||
/// This performs some initial work up front to build a log that is similar to
|
|
||||||
/// what Dolphin would construct; we're doing it here to avoid extra allocations
|
|
||||||
/// that would occur on the Dolphin logging side otherwise.
|
|
||||||
pub fn new(metadata: &Metadata, log_type: &str) -> Self {
|
|
||||||
let file = metadata.file().unwrap_or("");
|
|
||||||
let line = metadata.line().unwrap_or(0);
|
|
||||||
let level = metadata.level();
|
|
||||||
|
|
||||||
// Dolphin logs in the format of {Minutes}:{Seconds}:{Milliseconds}.
|
|
||||||
let time = OffsetDateTime::now_local().unwrap_or_else(|e| {
|
|
||||||
eprintln!("[dolphin_logger/layer.rs] Failed to get local time: {:?}", e);
|
|
||||||
|
|
||||||
// This will only happen if, for whatever reason, the timezone offset
|
|
||||||
// on the current system cannot be determined. Frankly there's bigger issues
|
|
||||||
// than logging if that's the case.
|
|
||||||
OffsetDateTime::now_utc()
|
|
||||||
});
|
|
||||||
|
|
||||||
let mins = time.minute();
|
|
||||||
let secs = time.second();
|
|
||||||
let millsecs = time.millisecond();
|
|
||||||
|
|
||||||
// We want 0-padded mins/secs, but we don't need the entire formatting infra
|
|
||||||
// that time would use - and this is simple enough to just do in a few lines.
|
|
||||||
let mp = match mins < 10 {
|
|
||||||
true => "0",
|
|
||||||
false => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let sp = match secs < 10 {
|
|
||||||
true => "0",
|
|
||||||
false => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
Self(format!(
|
|
||||||
"{mp}{mins}:{sp}{secs}:{millsecs} {file}:{line} {level}[{log_type}]: "
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Dolphin log window needs a newline attached to the end, so we just write one
|
|
||||||
/// as a finishing method.
|
|
||||||
pub fn finish(mut self) -> String {
|
|
||||||
if let Err(e) = write!(&mut self.0, "\n") {
|
|
||||||
eprintln!("Failed to finish logging string: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tracing::field::Visit for DolphinLoggerVisitor {
|
|
||||||
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_i128(&mut self, field: &tracing::field::Field, value: i128) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_u128(&mut self, field: &tracing::field::Field, value: u128) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_bool: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_str: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_error(&mut self, field: &tracing::field::Field, value: &(dyn std::error::Error + 'static)) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
|
||||||
if let Err(e) = write!(&mut self.0, "{}={:?} ", field.name(), value) {
|
|
||||||
eprintln!("Failed to record_debug: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
//! This library provides a tracing subscriber configuration that works with the
|
|
||||||
//! Dolphin logging setup.
|
|
||||||
//!
|
|
||||||
//! It essentially maps the concept of a `LogContainer` over to Rust, and provides
|
|
||||||
//! hooks to forward state change calls in. On top of that, this module contains
|
|
||||||
//! a custom `tracing_subscriber::Layer` that will pass logs back to Dolphin.
|
|
||||||
//!
|
|
||||||
//! Ultimately this should mean no log fragmentation or confusion.
|
|
||||||
|
|
||||||
use std::ffi::{c_char, c_int, CStr};
|
|
||||||
use std::sync::{Arc, Once, OnceLock, RwLock};
|
|
||||||
|
|
||||||
use tracing::Level;
|
|
||||||
use tracing_subscriber::prelude::*;
|
|
||||||
|
|
||||||
mod layer;
|
|
||||||
use layer::{convert_dolphin_log_level_to_tracing_level, DolphinLoggerLayer};
|
|
||||||
|
|
||||||
/// A type that mirrors a function over on the C++ side; because the library exists as
|
|
||||||
/// a dylib, it can't depend on any functions from the host application - but we _can_
|
|
||||||
/// pass in a hook/callback fn.
|
|
||||||
///
|
|
||||||
/// This should correspond to:
|
|
||||||
///
|
|
||||||
/// ``` notest
|
|
||||||
/// void LogFn(level, log_type, msg);
|
|
||||||
/// ```
|
|
||||||
pub(crate) type ForeignLoggerFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
|
|
||||||
|
|
||||||
/// A marker for where logs should be routed to.
|
|
||||||
///
|
|
||||||
/// Rust enum variants can't be strings, but we want to be able to pass an
|
|
||||||
/// enum to the tracing macro `target` field - which requires a static str.
|
|
||||||
///
|
|
||||||
/// Thus we'll fake things a bit and just expose a module that keys things
|
|
||||||
/// accordingly. The syntax will be the same as if using an enum.
|
|
||||||
///
|
|
||||||
/// If you want to add a new logger type, you will need to add a new value here
|
|
||||||
/// and create a corresponding `LogContainer` on the Dolphin side with the corresponding
|
|
||||||
/// tag. The rest should "just work".
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[allow(non_upper_case_globals)]
|
|
||||||
pub mod Log {
|
|
||||||
/// Used for dumping logs from dependencies that we may need to inspect.
|
|
||||||
/// This may also get logs that are not properly tagged! If that happens, you need
|
|
||||||
/// to fix your logging calls. :)
|
|
||||||
pub const DEPENDENCIES: &'static str = "SLIPPI_RUST_DEPENDENCIES";
|
|
||||||
|
|
||||||
/// The default target for EXI tracing.
|
|
||||||
pub const EXI: &'static str = "SLIPPI_RUST_EXI";
|
|
||||||
|
|
||||||
/// Logs for `SlippiGameReporter`.
|
|
||||||
pub const GameReporter: &'static str = "SLIPPI_RUST_GAME_REPORTER";
|
|
||||||
|
|
||||||
/// Can be used to segment Jukebox logs.
|
|
||||||
pub const Jukebox: &'static str = "SLIPPI_RUST_JUKEBOX";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a `LogContainer` on the Dolphin side.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct LogContainer {
|
|
||||||
kind: String,
|
|
||||||
log_type: c_int,
|
|
||||||
is_enabled: bool,
|
|
||||||
level: Level,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A global stack of `LogContainers`.
|
|
||||||
///
|
|
||||||
/// All logger registrations (which require `write`) should happen up-front due to how
|
|
||||||
/// Dolphin itself works. RwLock here should provide us parallel reader access after.
|
|
||||||
static LOG_CONTAINERS: OnceLock<Arc<RwLock<Vec<LogContainer>>>> = OnceLock::new();
|
|
||||||
|
|
||||||
/// This should be called from the Dolphin LogManager initialization to ensure that
|
|
||||||
/// all logging needs on the Rust side are configured appropriately.
|
|
||||||
///
|
|
||||||
/// *Usually* you do not want a library installing a global logger, however our use case is
|
|
||||||
/// not so standard: this library does in a sense act as an application due to the way it's
|
|
||||||
/// called into, and we *want* a global subscriber.
|
|
||||||
pub fn init(logger_fn: ForeignLoggerFn) {
|
|
||||||
let _containers = LOG_CONTAINERS.get_or_init(|| Arc::new(RwLock::new(Vec::new())));
|
|
||||||
|
|
||||||
// A guard so that we can't double-init logging layers.
|
|
||||||
static LOGGER: Once = Once::new();
|
|
||||||
|
|
||||||
// We don't use `try_init` here because we do want to
|
|
||||||
// know if something else, somehow, registered before us.
|
|
||||||
LOGGER.call_once(|| {
|
|
||||||
// We do this so that full backtrace's are emitted on any crashes.
|
|
||||||
std::env::set_var("RUST_BACKTRACE", "full");
|
|
||||||
|
|
||||||
tracing_subscriber::registry().with(DolphinLoggerLayer::new(logger_fn)).init();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a log container, which mirrors a Dolphin `LogContainer`.
|
|
||||||
///
|
|
||||||
/// This enables passing a configured log level and/or enabled status across the boundary from
|
|
||||||
/// Dolphin to our tracing subscriber setup. This is important as we want to short-circuit any
|
|
||||||
/// allocations during log handling that aren't necessary (e.g if a log is outright disabled).
|
|
||||||
pub fn register_container(kind: *const c_char, log_type: c_int, is_enabled: bool, default_log_level: c_int) {
|
|
||||||
// We control the other end of the registration flow, so we can ensure this ptr's valid UTF-8.
|
|
||||||
let c_kind_str = unsafe { CStr::from_ptr(kind) };
|
|
||||||
|
|
||||||
let kind = c_kind_str
|
|
||||||
.to_str()
|
|
||||||
.expect("[dolphin_logger::register_container]: Failed to convert kind c_char to str")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let containers = LOG_CONTAINERS
|
|
||||||
.get()
|
|
||||||
.expect("[dolphin_logger::register_container]: Attempting to get `LOG_CONTAINERS` before init");
|
|
||||||
|
|
||||||
let mut writer = containers
|
|
||||||
.write()
|
|
||||||
.expect("[dolphin_logger::register_container]: Unable to acquire write lock on `LOG_CONTAINERS`?");
|
|
||||||
|
|
||||||
(*writer).push(LogContainer {
|
|
||||||
kind,
|
|
||||||
log_type,
|
|
||||||
is_enabled,
|
|
||||||
level: convert_dolphin_log_level_to_tracing_level(default_log_level),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a particular log container to a new enabled state. When a log container is in a disabled
|
|
||||||
/// state, no allocations will happen behind the scenes for any logging period.
|
|
||||||
pub fn update_container(kind: *const c_char, enabled: bool, level: c_int) {
|
|
||||||
// We control the other end of the registration flow, so we can ensure this ptr's valid UTF-8.
|
|
||||||
let c_kind_str = unsafe { CStr::from_ptr(kind) };
|
|
||||||
|
|
||||||
let kind = c_kind_str
|
|
||||||
.to_str()
|
|
||||||
.expect("[dolphin_logger::update_container]: Failed to convert kind c_char to str");
|
|
||||||
|
|
||||||
let containers = LOG_CONTAINERS
|
|
||||||
.get()
|
|
||||||
.expect("[dolphin_logger::update_container]: Attempting to get `LOG_CONTAINERS` before init");
|
|
||||||
|
|
||||||
let mut writer = containers
|
|
||||||
.write()
|
|
||||||
.expect("[dolphin_logger::update_container]: Unable to acquire write lock on `LOG_CONTAINERS`?");
|
|
||||||
|
|
||||||
for container in (*writer).iter_mut() {
|
|
||||||
if container.kind == kind {
|
|
||||||
container.is_enabled = enabled;
|
|
||||||
container.level = convert_dolphin_log_level_to_tracing_level(level);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mainline doesn't have levels per container, but we'll keep it that way to avoid diverging the codebase.
|
|
||||||
/// Once Ishii dies we should remove this and refactor all this.
|
|
||||||
pub fn mainline_update_log_level(level: c_int) {
|
|
||||||
let containers = LOG_CONTAINERS
|
|
||||||
.get()
|
|
||||||
.expect("[dolphin_logger::mainline_update_log_level]: Attempting to get `LOG_CONTAINERS` before init");
|
|
||||||
|
|
||||||
let mut writer = containers
|
|
||||||
.write()
|
|
||||||
.expect("[dolphin_logger::mainline_update_log_level]: Unable to acquire write lock on `LOG_CONTAINERS`?");
|
|
||||||
|
|
||||||
for container in (*writer).iter_mut() {
|
|
||||||
container.level = convert_dolphin_log_level_to_tracing_level(level);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
//! Implements types and some helpers used in rendering messages via the On-Screen-Display
|
|
||||||
//! functionality in Dolphin.
|
|
||||||
|
|
||||||
use std::ffi::c_char;
|
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
/// A type that mirrors.
|
|
||||||
pub type AddOSDMessageFn = unsafe extern "C" fn(*const c_char, u32, u32);
|
|
||||||
|
|
||||||
/// Global holder for our message handler fn.
|
|
||||||
pub(crate) static MESSAGE_HOOK: OnceLock<AddOSDMessageFn> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Sets the global hook for sending an On-Screen-Display message back to Dolphin.
|
|
||||||
///
|
|
||||||
/// Once set, this hook cannot be changed - there's no scenario where we expect to
|
|
||||||
/// ever need to do that anyway.
|
|
||||||
pub fn set_global_hook(add_osd_message_fn: AddOSDMessageFn) {
|
|
||||||
if MESSAGE_HOOK.get().is_none() {
|
|
||||||
MESSAGE_HOOK.set(add_osd_message_fn).expect("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents colors that the On-Screen-Display could render.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum Color {
|
|
||||||
Cyan,
|
|
||||||
Green,
|
|
||||||
Red,
|
|
||||||
Yellow,
|
|
||||||
Custom(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Color {
|
|
||||||
/// Returns a `u32` representation of this type.
|
|
||||||
pub fn to_u32(&self) -> u32 {
|
|
||||||
match self {
|
|
||||||
Self::Cyan => 0xFF00FFFF,
|
|
||||||
Self::Green => 0xFF00FF00,
|
|
||||||
Self::Red => 0xFFFF0000,
|
|
||||||
Self::Yellow => 0xFFFFFF30,
|
|
||||||
Self::Custom(value) => *value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the length of time an On-Screen-Display message should stay on screen.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub enum Duration {
|
|
||||||
Short,
|
|
||||||
Normal,
|
|
||||||
VeryLong,
|
|
||||||
Custom(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Duration {
|
|
||||||
/// Returns a u32 representation of this type.
|
|
||||||
pub fn to_u32(&self) -> u32 {
|
|
||||||
match self {
|
|
||||||
Self::Short => 2000,
|
|
||||||
Self::Normal => 5000,
|
|
||||||
Self::VeryLong => 10000,
|
|
||||||
Self::Custom(value) => *value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
21
Externals/SlippiRustExtensions/exi/Cargo.toml
vendored
21
Externals/SlippiRustExtensions/exi/Cargo.toml
vendored
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "slippi-exi-device"
|
|
||||||
description = "Implements a shadow EXI device."
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = [
|
|
||||||
"Slippi Team",
|
|
||||||
"Ryan McGrath <ryan@rymc.io>"
|
|
||||||
]
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
ishiiruka = []
|
|
||||||
mainline = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dolphin-integrations = { path = "../dolphin" }
|
|
||||||
slippi-game-reporter = { path = "../game-reporter" }
|
|
||||||
slippi-jukebox = { path = "../jukebox" }
|
|
||||||
tracing = { workspace = true }
|
|
6
Externals/SlippiRustExtensions/exi/README.md
vendored
6
Externals/SlippiRustExtensions/exi/README.md
vendored
|
@ -1,6 +0,0 @@
|
||||||
# Slippi Rust EXI Device
|
|
||||||
This module acts as a "shadow" EXI device for the _actual_ C++ EXI Device ([`EXI_DeviceSlippi.cpp`](../../../Source/Core/Core/HW/EXI_DeviceSlippi.cpp)) over on the Dolphin side of things. The C++ class _owns_ this EXI device and acts as the general entry point to everything.
|
|
||||||
|
|
||||||
If you're building something on the Rust side, you probably - unless it's a standalone function or you really know it needs to be global - want to just hook it here. Doing so will ensure you get the automatic lifecycle management and can avoid worrying about any Rust cleanup pieces (just implement `Drop`).
|
|
||||||
|
|
||||||
For an example of this pattern, check out how the `Jukebox` is orchestrated in this crate.
|
|
68
Externals/SlippiRustExtensions/exi/src/lib.rs
vendored
68
Externals/SlippiRustExtensions/exi/src/lib.rs
vendored
|
@ -1,68 +0,0 @@
|
||||||
//! This module houses the `SlippiEXIDevice`, which is in effect a "shadow subclass" of the C++
|
|
||||||
//! Slippi EXI device.
|
|
||||||
//!
|
|
||||||
//! What this means is that the Slippi EXI Device (C++) holds a pointer to the Rust
|
|
||||||
//! `SlippiEXIDevice` and forwards calls over the C FFI. This has a fairly clean mapping to "when
|
|
||||||
//! Slippi stuff is happening" and enables us to let the Rust side live in its own world.
|
|
||||||
|
|
||||||
use dolphin_integrations::Log;
|
|
||||||
use slippi_game_reporter::SlippiGameReporter;
|
|
||||||
use slippi_jukebox::Jukebox;
|
|
||||||
|
|
||||||
/// An EXI Device subclass specific to managing and interacting with the game itself.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SlippiEXIDevice {
|
|
||||||
iso_path: String,
|
|
||||||
pub game_reporter: SlippiGameReporter,
|
|
||||||
jukebox: Option<Jukebox>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlippiEXIDevice {
|
|
||||||
/// Creates and returns a new `SlippiEXIDevice` with default values.
|
|
||||||
///
|
|
||||||
/// At the moment you should never need to call this yourself.
|
|
||||||
pub fn new(iso_path: String) -> Self {
|
|
||||||
tracing::info!(target: Log::EXI, "Starting SlippiEXIDevice");
|
|
||||||
|
|
||||||
let game_reporter = SlippiGameReporter::new(iso_path.clone());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
iso_path,
|
|
||||||
game_reporter,
|
|
||||||
jukebox: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stubbed for now, but this would get called by the C++ EXI device on DMAWrite.
|
|
||||||
pub fn dma_write(&mut self, _address: usize, _size: usize) {}
|
|
||||||
|
|
||||||
/// Stubbed for now, but this would get called by the C++ EXI device on DMARead.
|
|
||||||
pub fn dma_read(&mut self, _address: usize, _size: usize) {}
|
|
||||||
|
|
||||||
/// Configures a new Jukebox, or ensures an existing one is dropped if it's being disabled.
|
|
||||||
pub fn configure_jukebox(
|
|
||||||
&mut self,
|
|
||||||
is_enabled: bool,
|
|
||||||
m_p_ram: *const u8,
|
|
||||||
get_dolphin_volume_fn: slippi_jukebox::ForeignGetVolumeFn,
|
|
||||||
) {
|
|
||||||
if !is_enabled {
|
|
||||||
self.jukebox = None;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match Jukebox::new(m_p_ram, self.iso_path.clone(), get_dolphin_volume_fn) {
|
|
||||||
Ok(jukebox) => {
|
|
||||||
self.jukebox = Some(jukebox);
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::EXI,
|
|
||||||
error = ?e,
|
|
||||||
"Failed to start Jukebox"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
33
Externals/SlippiRustExtensions/ffi/Cargo.toml
vendored
33
Externals/SlippiRustExtensions/ffi/Cargo.toml
vendored
|
@ -1,33 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "slippi_rust_extensions"
|
|
||||||
description = "An internal library that exposes entry points via the C FFI."
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = [
|
|
||||||
"Slippi Team",
|
|
||||||
"Ryan McGrath <ryan@rymc.io>"
|
|
||||||
]
|
|
||||||
repository = ""
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
cbindgen = "0.24.3"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["ishiiruka"]
|
|
||||||
ishiiruka = [
|
|
||||||
"dolphin-integrations/ishiiruka",
|
|
||||||
"slippi-game-reporter/ishiiruka",
|
|
||||||
"slippi-exi-device/ishiiruka"
|
|
||||||
]
|
|
||||||
mainline = []
|
|
||||||
playback = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dolphin-integrations = { path = "../dolphin" }
|
|
||||||
slippi-game-reporter = { path = "../game-reporter" }
|
|
||||||
slippi-exi-device = { path = "../exi" }
|
|
||||||
tracing = { workspace = true }
|
|
8
Externals/SlippiRustExtensions/ffi/README.md
vendored
8
Externals/SlippiRustExtensions/ffi/README.md
vendored
|
@ -1,8 +0,0 @@
|
||||||
# Slippi Rust FFI
|
|
||||||
This crate contains the external API that Dolphin can see and interact with. The API itself is a small set of C functions that handle creating/destroying/updating various components (EXI, Logger, Jukebox, etc) on the Rust side of things.
|
|
||||||
|
|
||||||
The included `build.rs` will automatically generate a C header set on each build, and the CMake and Visual Studio projects are configured to locate it from the `includes` folder.
|
|
||||||
|
|
||||||
When adding a new method to this crate, prefix it with `slprs_` so that the Dolphin codebase keeps a clear delineation of where Rust code is being used. If (or when) you use `unsafe`, please add a comment explaining _why_ we can guarantee some aspect of safety.
|
|
||||||
|
|
||||||
See the method headers for more information.
|
|
28
Externals/SlippiRustExtensions/ffi/build.rs
vendored
28
Externals/SlippiRustExtensions/ffi/build.rs
vendored
|
@ -1,28 +0,0 @@
|
||||||
//! This build script simply generates C FFI bindings for the freestanding
|
|
||||||
//! functions in `lib.rs` and dumps them into a header that the Dolphin
|
|
||||||
//! project is pre-configured to find.
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
||||||
|
|
||||||
// We disable `enum class` declarations to mirror what Ishiiruka
|
|
||||||
// currently does.
|
|
||||||
let enum_config = cbindgen::EnumConfig {
|
|
||||||
enum_class: false,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = cbindgen::Config {
|
|
||||||
enumeration: enum_config,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
cbindgen::Builder::new()
|
|
||||||
.with_config(config)
|
|
||||||
.with_crate(crate_dir)
|
|
||||||
.generate()
|
|
||||||
.expect("Unable to generate bindings")
|
|
||||||
.write_to_file("includes/SlippiRustExtensions.h");
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
#include <cstdarg>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <ostream>
|
|
||||||
#include <new>
|
|
||||||
|
|
||||||
/// This enum is duplicated from `slippi_game_reporter::OnlinePlayMode` in order
|
|
||||||
/// to appease cbindgen, which cannot see the type from the other module for
|
|
||||||
/// inspection.
|
|
||||||
///
|
|
||||||
/// This enum will likely go away as things move towards Rust, since it's effectively
|
|
||||||
/// just C FFI glue code.
|
|
||||||
enum SlippiMatchmakingOnlinePlayMode {
|
|
||||||
Ranked = 0,
|
|
||||||
Unranked = 1,
|
|
||||||
Direct = 2,
|
|
||||||
Teams = 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
/// Creates and leaks a shadow EXI device.
|
|
||||||
///
|
|
||||||
/// The C++ (Dolphin) side of things should call this and pass the appropriate arguments. At
|
|
||||||
/// that point, everything on the Rust side is its own universe, and should be told to shut
|
|
||||||
/// down (at whatever point) via the corresponding `slprs_exi_device_destroy` function.
|
|
||||||
///
|
|
||||||
/// The returned pointer from this should *not* be used after calling `slprs_exi_device_destroy`.
|
|
||||||
uintptr_t slprs_exi_device_create(const char *iso_path,
|
|
||||||
void (*osd_add_msg_fn)(const char*, uint32_t, uint32_t));
|
|
||||||
|
|
||||||
/// The C++ (Dolphin) side of things should call this to notify the Rust side that it
|
|
||||||
/// can safely shut down and clean up.
|
|
||||||
void slprs_exi_device_destroy(uintptr_t exi_device_instance_ptr);
|
|
||||||
|
|
||||||
/// This method should be called from the EXI device subclass shim that's registered on
|
|
||||||
/// the Dolphin side, corresponding to:
|
|
||||||
///
|
|
||||||
/// `virtual void DMAWrite(u32 _uAddr, u32 _uSize);`
|
|
||||||
void slprs_exi_device_dma_write(uintptr_t exi_device_instance_ptr,
|
|
||||||
const uint8_t *address,
|
|
||||||
const uint8_t *size);
|
|
||||||
|
|
||||||
/// This method should be called from the EXI device subclass shim that's registered on
|
|
||||||
/// the Dolphin side, corresponding to:
|
|
||||||
///
|
|
||||||
/// `virtual void DMARead(u32 _uAddr, u32 _uSize);`
|
|
||||||
void slprs_exi_device_dma_read(uintptr_t exi_device_instance_ptr,
|
|
||||||
const uint8_t *address,
|
|
||||||
const uint8_t *size);
|
|
||||||
|
|
||||||
/// Moves ownership of the `GameReport` at the specified address to the
|
|
||||||
/// `SlippiGameReporter` on the EXI Device the corresponding address. This
|
|
||||||
/// will then add it to the processing pipeline.
|
|
||||||
///
|
|
||||||
/// The reporter will manage the actual... reporting.
|
|
||||||
void slprs_exi_device_log_game_report(uintptr_t instance_ptr, uintptr_t game_report_instance_ptr);
|
|
||||||
|
|
||||||
/// Calls through to `SlippiGameReporter::start_new_session`.
|
|
||||||
void slprs_exi_device_start_new_reporter_session(uintptr_t instance_ptr);
|
|
||||||
|
|
||||||
/// Calls through to the `SlippiGameReporter` on the EXI device to report a
|
|
||||||
/// match completion event.
|
|
||||||
void slprs_exi_device_report_match_completion(uintptr_t instance_ptr,
|
|
||||||
const char *uid,
|
|
||||||
const char *play_key,
|
|
||||||
const char *match_id,
|
|
||||||
uint8_t end_mode);
|
|
||||||
|
|
||||||
/// Calls through to the `SlippiGameReporter` on the EXI device to report a
|
|
||||||
/// match abandon event.
|
|
||||||
void slprs_exi_device_report_match_abandonment(uintptr_t instance_ptr,
|
|
||||||
const char *uid,
|
|
||||||
const char *play_key,
|
|
||||||
const char *match_id);
|
|
||||||
|
|
||||||
/// Calls through to `SlippiGameReporter::push_replay_data`.
|
|
||||||
void slprs_exi_device_reporter_push_replay_data(uintptr_t instance_ptr,
|
|
||||||
const uint8_t *data,
|
|
||||||
uint32_t length);
|
|
||||||
|
|
||||||
/// Configures the Jukebox process. This needs to be called after the EXI device is created
|
|
||||||
/// in order for certain pieces of Dolphin to be properly initalized; this may change down
|
|
||||||
/// the road though and is not set in stone.
|
|
||||||
void slprs_exi_device_configure_jukebox(uintptr_t exi_device_instance_ptr,
|
|
||||||
bool is_enabled,
|
|
||||||
const uint8_t *m_p_ram,
|
|
||||||
int (*get_dolphin_volume_fn)());
|
|
||||||
|
|
||||||
/// Creates a new Player Report and leaks it, returning the pointer.
|
|
||||||
///
|
|
||||||
/// This should be passed on to a GameReport for processing.
|
|
||||||
uintptr_t slprs_player_report_create(const char *uid,
|
|
||||||
uint8_t slot_type,
|
|
||||||
double damage_done,
|
|
||||||
uint8_t stocks_remaining,
|
|
||||||
uint8_t character_id,
|
|
||||||
uint8_t color_id,
|
|
||||||
int64_t starting_stocks,
|
|
||||||
int64_t starting_percent);
|
|
||||||
|
|
||||||
/// Creates a new GameReport and leaks it, returning the instance pointer
|
|
||||||
/// after doing so.
|
|
||||||
///
|
|
||||||
/// This is expected to ultimately be passed to the game reporter, which will handle
|
|
||||||
/// destruction and cleanup.
|
|
||||||
uintptr_t slprs_game_report_create(const char *uid,
|
|
||||||
const char *play_key,
|
|
||||||
SlippiMatchmakingOnlinePlayMode online_mode,
|
|
||||||
const char *match_id,
|
|
||||||
uint32_t duration_frames,
|
|
||||||
uint32_t game_index,
|
|
||||||
uint32_t tie_break_index,
|
|
||||||
int8_t winner_index,
|
|
||||||
uint8_t game_end_method,
|
|
||||||
int8_t lras_initiator,
|
|
||||||
int32_t stage_id);
|
|
||||||
|
|
||||||
/// Takes ownership of the `PlayerReport` at the specified pointer, adding it to the
|
|
||||||
/// `GameReport` at the corresponding pointer.
|
|
||||||
void slprs_game_report_add_player_report(uintptr_t instance_ptr,
|
|
||||||
uintptr_t player_report_instance_ptr);
|
|
||||||
|
|
||||||
/// This should be called from the Dolphin LogManager initialization to ensure that
|
|
||||||
/// all logging needs on the Rust side are configured appropriately.
|
|
||||||
///
|
|
||||||
/// For more information, consult `dolphin_logger::init`.
|
|
||||||
///
|
|
||||||
/// Note that `logger_fn` cannot be type-aliased here, otherwise cbindgen will
|
|
||||||
/// mess up the header output. That said, the function type represents:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// void Log(level, log_type, msg);
|
|
||||||
/// ```
|
|
||||||
void slprs_logging_init(void (*logger_fn)(int, int, const char*));
|
|
||||||
|
|
||||||
/// Registers a log container, which mirrors a Dolphin `LogContainer` (`RustLogContainer`).
|
|
||||||
///
|
|
||||||
/// See `dolphin_logger::register_container` for more information.
|
|
||||||
void slprs_logging_register_container(const char *kind,
|
|
||||||
int log_type,
|
|
||||||
bool is_enabled,
|
|
||||||
int default_log_level);
|
|
||||||
|
|
||||||
/// Updates the configuration for a registered logging container.
|
|
||||||
///
|
|
||||||
/// For more information, see `dolphin_logger::update_container`.
|
|
||||||
void slprs_logging_update_container(const char *kind, bool enabled, int level);
|
|
||||||
|
|
||||||
/// Updates the configuration for registered logging container on mainline
|
|
||||||
///
|
|
||||||
/// For more information, see `dolphin_logger::update_container`.
|
|
||||||
void slprs_mainline_logging_update_log_level(int level);
|
|
||||||
|
|
||||||
} // extern "C"
|
|
209
Externals/SlippiRustExtensions/ffi/src/exi.rs
vendored
209
Externals/SlippiRustExtensions/ffi/src/exi.rs
vendored
|
@ -1,209 +0,0 @@
|
||||||
use std::ffi::{c_char, c_int};
|
|
||||||
|
|
||||||
use dolphin_integrations::Log;
|
|
||||||
use slippi_exi_device::SlippiEXIDevice;
|
|
||||||
use slippi_game_reporter::GameReport;
|
|
||||||
|
|
||||||
use crate::c_str_to_string;
|
|
||||||
|
|
||||||
/// Creates and leaks a shadow EXI device.
|
|
||||||
///
|
|
||||||
/// The C++ (Dolphin) side of things should call this and pass the appropriate arguments. At
|
|
||||||
/// that point, everything on the Rust side is its own universe, and should be told to shut
|
|
||||||
/// down (at whatever point) via the corresponding `slprs_exi_device_destroy` function.
|
|
||||||
///
|
|
||||||
/// The returned pointer from this should *not* be used after calling `slprs_exi_device_destroy`.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_create(
|
|
||||||
iso_path: *const c_char,
|
|
||||||
osd_add_msg_fn: unsafe extern "C" fn(*const c_char, u32, u32),
|
|
||||||
) -> usize {
|
|
||||||
let fn_name = "slprs_exi_device_create";
|
|
||||||
|
|
||||||
let iso_path = c_str_to_string(iso_path, fn_name, "iso_path");
|
|
||||||
|
|
||||||
dolphin_integrations::ffi::osd::set_global_hook(osd_add_msg_fn);
|
|
||||||
|
|
||||||
let exi_device = Box::new(SlippiEXIDevice::new(iso_path));
|
|
||||||
let exi_device_instance_ptr = Box::into_raw(exi_device) as usize;
|
|
||||||
|
|
||||||
tracing::warn!(target: Log::EXI, ptr = exi_device_instance_ptr, "Creating Device");
|
|
||||||
|
|
||||||
exi_device_instance_ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The C++ (Dolphin) side of things should call this to notify the Rust side that it
|
|
||||||
/// can safely shut down and clean up.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_destroy(exi_device_instance_ptr: usize) {
|
|
||||||
tracing::warn!(target: Log::EXI, ptr = exi_device_instance_ptr, "Destroying Device");
|
|
||||||
|
|
||||||
// Coerce the instance from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `exi_device_instance_ptr` is only owned
|
|
||||||
// by the C++ EXI device, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
unsafe {
|
|
||||||
// Coerce ownership back, then let standard Drop semantics apply
|
|
||||||
let _device = Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method should be called from the EXI device subclass shim that's registered on
|
|
||||||
/// the Dolphin side, corresponding to:
|
|
||||||
///
|
|
||||||
/// `virtual void DMAWrite(u32 _uAddr, u32 _uSize);`
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_dma_write(exi_device_instance_ptr: usize, address: *const u8, size: *const u8) {
|
|
||||||
// Coerce the instance back from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `exi_device_instance_ptr` pointer is only owned
|
|
||||||
// by the C++ EXI device, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut device = unsafe { Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
device.dma_write(address as usize, size as usize);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method should be called from the EXI device subclass shim that's registered on
|
|
||||||
/// the Dolphin side, corresponding to:
|
|
||||||
///
|
|
||||||
/// `virtual void DMARead(u32 _uAddr, u32 _uSize);`
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_dma_read(exi_device_instance_ptr: usize, address: *const u8, size: *const u8) {
|
|
||||||
// Coerce the instance from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `exi_device_instance_ptr` pointer is only owned
|
|
||||||
// by the C++ EXI device, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut device = unsafe { Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
device.dma_read(address as usize, size as usize);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves ownership of the `GameReport` at the specified address to the
|
|
||||||
/// `SlippiGameReporter` on the EXI Device the corresponding address. This
|
|
||||||
/// will then add it to the processing pipeline.
|
|
||||||
///
|
|
||||||
/// The reporter will manage the actual... reporting.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_log_game_report(instance_ptr: usize, game_report_instance_ptr: usize) {
|
|
||||||
// Coerce the instances from the pointers. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the pointers are only owned
|
|
||||||
// by us, and are created/destroyed with the corresponding lifetimes.
|
|
||||||
let (mut device, game_report) = unsafe {
|
|
||||||
(
|
|
||||||
Box::from_raw(instance_ptr as *mut SlippiEXIDevice),
|
|
||||||
Box::from_raw(game_report_instance_ptr as *mut GameReport),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
device.game_reporter.log_report(*game_report);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls through to `SlippiGameReporter::start_new_session`.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_start_new_reporter_session(instance_ptr: usize) {
|
|
||||||
// Coerce the instances from the pointers. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the pointers are only owned
|
|
||||||
// by us, and are created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut device = unsafe { Box::from_raw(instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
device.game_reporter.start_new_session();
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls through to the `SlippiGameReporter` on the EXI device to report a
|
|
||||||
/// match completion event.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_report_match_completion(
|
|
||||||
instance_ptr: usize,
|
|
||||||
uid: *const c_char,
|
|
||||||
play_key: *const c_char,
|
|
||||||
match_id: *const c_char,
|
|
||||||
end_mode: u8,
|
|
||||||
) {
|
|
||||||
// Coerce the instances from the pointers. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the pointers are only owned
|
|
||||||
// by us, and are created/destroyed with the corresponding lifetimes.
|
|
||||||
let device = unsafe { Box::from_raw(instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
let fn_name = "slprs_exi_device_report_match_completion";
|
|
||||||
let uid = c_str_to_string(uid, fn_name, "uid");
|
|
||||||
let play_key = c_str_to_string(play_key, fn_name, "play_key");
|
|
||||||
let match_id = c_str_to_string(match_id, fn_name, "match_id");
|
|
||||||
|
|
||||||
device.game_reporter.report_completion(uid, play_key, match_id, end_mode);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls through to the `SlippiGameReporter` on the EXI device to report a
|
|
||||||
/// match abandon event.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_report_match_abandonment(
|
|
||||||
instance_ptr: usize,
|
|
||||||
uid: *const c_char,
|
|
||||||
play_key: *const c_char,
|
|
||||||
match_id: *const c_char,
|
|
||||||
) {
|
|
||||||
// Coerce the instances from the pointers. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the pointers are only owned
|
|
||||||
// by us, and are created/destroyed with the corresponding lifetimes.
|
|
||||||
let device = unsafe { Box::from_raw(instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
let fn_name = "slprs_exi_device_report_match_abandonment";
|
|
||||||
let uid = c_str_to_string(uid, fn_name, "uid");
|
|
||||||
let play_key = c_str_to_string(play_key, fn_name, "play_key");
|
|
||||||
let match_id = c_str_to_string(match_id, fn_name, "match_id");
|
|
||||||
|
|
||||||
device.game_reporter.report_abandonment(uid, play_key, match_id);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls through to `SlippiGameReporter::push_replay_data`.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_reporter_push_replay_data(instance_ptr: usize, data: *const u8, length: u32) {
|
|
||||||
// Convert our pointer to a Rust slice so that the game reporter
|
|
||||||
// doesn't need to deal with anything C-ish.
|
|
||||||
let slice = unsafe { std::slice::from_raw_parts(data, length as usize) };
|
|
||||||
|
|
||||||
// Coerce the instances from the pointers. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the pointers are only owned
|
|
||||||
// by us, and are created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut device = unsafe { Box::from_raw(instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
device.game_reporter.push_replay_data(slice);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configures the Jukebox process. This needs to be called after the EXI device is created
|
|
||||||
/// in order for certain pieces of Dolphin to be properly initalized; this may change down
|
|
||||||
/// the road though and is not set in stone.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_exi_device_configure_jukebox(
|
|
||||||
exi_device_instance_ptr: usize,
|
|
||||||
is_enabled: bool,
|
|
||||||
m_p_ram: *const u8,
|
|
||||||
get_dolphin_volume_fn: unsafe extern "C" fn() -> c_int,
|
|
||||||
) {
|
|
||||||
// Coerce the instance from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `exi_device_instance_ptr` is only owned
|
|
||||||
// by the C++ EXI device, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut device = unsafe { Box::from_raw(exi_device_instance_ptr as *mut SlippiEXIDevice) };
|
|
||||||
|
|
||||||
device.configure_jukebox(is_enabled, m_p_ram, get_dolphin_volume_fn);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(device);
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
use std::ffi::c_char;
|
|
||||||
|
|
||||||
use slippi_game_reporter::{GameReport, OnlinePlayMode as ReporterOnlinePlayMode, PlayerReport};
|
|
||||||
|
|
||||||
use crate::{c_str_to_string, set};
|
|
||||||
|
|
||||||
/// This enum is duplicated from `slippi_game_reporter::OnlinePlayMode` in order
|
|
||||||
/// to appease cbindgen, which cannot see the type from the other module for
|
|
||||||
/// inspection.
|
|
||||||
///
|
|
||||||
/// This enum will likely go away as things move towards Rust, since it's effectively
|
|
||||||
/// just C FFI glue code.
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[repr(C)]
|
|
||||||
pub enum SlippiMatchmakingOnlinePlayMode {
|
|
||||||
Ranked = 0,
|
|
||||||
Unranked = 1,
|
|
||||||
Direct = 2,
|
|
||||||
Teams = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new Player Report and leaks it, returning the pointer.
|
|
||||||
///
|
|
||||||
/// This should be passed on to a GameReport for processing.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_player_report_create(
|
|
||||||
uid: *const c_char,
|
|
||||||
slot_type: u8,
|
|
||||||
damage_done: f64,
|
|
||||||
stocks_remaining: u8,
|
|
||||||
character_id: u8,
|
|
||||||
color_id: u8,
|
|
||||||
starting_stocks: i64,
|
|
||||||
starting_percent: i64,
|
|
||||||
) -> usize {
|
|
||||||
let uid = c_str_to_string(uid, "slprs_player_report_create", "uid");
|
|
||||||
|
|
||||||
let report = Box::new(PlayerReport {
|
|
||||||
uid,
|
|
||||||
slot_type,
|
|
||||||
damage_done,
|
|
||||||
stocks_remaining,
|
|
||||||
character_id,
|
|
||||||
color_id,
|
|
||||||
starting_stocks,
|
|
||||||
starting_percent,
|
|
||||||
});
|
|
||||||
|
|
||||||
let report_instance_ptr = Box::into_raw(report) as usize;
|
|
||||||
|
|
||||||
report_instance_ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new GameReport and leaks it, returning the instance pointer
|
|
||||||
/// after doing so.
|
|
||||||
///
|
|
||||||
/// This is expected to ultimately be passed to the game reporter, which will handle
|
|
||||||
/// destruction and cleanup.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_game_report_create(
|
|
||||||
uid: *const c_char,
|
|
||||||
play_key: *const c_char,
|
|
||||||
online_mode: SlippiMatchmakingOnlinePlayMode,
|
|
||||||
match_id: *const c_char,
|
|
||||||
duration_frames: u32,
|
|
||||||
game_index: u32,
|
|
||||||
tie_break_index: u32,
|
|
||||||
winner_index: i8,
|
|
||||||
game_end_method: u8,
|
|
||||||
lras_initiator: i8,
|
|
||||||
stage_id: i32,
|
|
||||||
) -> usize {
|
|
||||||
let fn_name = "slprs_game_report_create";
|
|
||||||
let uid = c_str_to_string(uid, fn_name, "user_id");
|
|
||||||
let play_key = c_str_to_string(play_key, fn_name, "play_key");
|
|
||||||
let match_id = c_str_to_string(match_id, fn_name, "match_id");
|
|
||||||
|
|
||||||
let report = Box::new(GameReport {
|
|
||||||
uid,
|
|
||||||
play_key,
|
|
||||||
|
|
||||||
online_mode: match online_mode {
|
|
||||||
SlippiMatchmakingOnlinePlayMode::Ranked => ReporterOnlinePlayMode::Ranked,
|
|
||||||
SlippiMatchmakingOnlinePlayMode::Unranked => ReporterOnlinePlayMode::Unranked,
|
|
||||||
SlippiMatchmakingOnlinePlayMode::Direct => ReporterOnlinePlayMode::Direct,
|
|
||||||
SlippiMatchmakingOnlinePlayMode::Teams => ReporterOnlinePlayMode::Teams,
|
|
||||||
},
|
|
||||||
|
|
||||||
match_id,
|
|
||||||
attempts: 0,
|
|
||||||
duration_frames,
|
|
||||||
game_index,
|
|
||||||
tie_break_index,
|
|
||||||
winner_index,
|
|
||||||
game_end_method,
|
|
||||||
lras_initiator,
|
|
||||||
stage_id,
|
|
||||||
players: Vec::new(),
|
|
||||||
replay_data: Vec::new(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let report_instance_ptr = Box::into_raw(report) as usize;
|
|
||||||
|
|
||||||
report_instance_ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes ownership of the `PlayerReport` at the specified pointer, adding it to the
|
|
||||||
/// `GameReport` at the corresponding pointer.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_game_report_add_player_report(instance_ptr: usize, player_report_instance_ptr: usize) {
|
|
||||||
// Coerce the instance from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `game_report_instance_ptr` is only owned
|
|
||||||
// by us, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
let player_report = unsafe { Box::from_raw(player_report_instance_ptr as *mut PlayerReport) };
|
|
||||||
|
|
||||||
set::<GameReport, _>(instance_ptr, move |report| {
|
|
||||||
report.players.push(*player_report);
|
|
||||||
});
|
|
||||||
}
|
|
58
Externals/SlippiRustExtensions/ffi/src/lib.rs
vendored
58
Externals/SlippiRustExtensions/ffi/src/lib.rs
vendored
|
@ -1,58 +0,0 @@
|
||||||
//! This library is the core interface for the Rust side of things, and consists
|
|
||||||
//! predominantly of C FFI bridging functions that can be called from the Dolphin
|
|
||||||
//! side of things.
|
|
||||||
//!
|
|
||||||
//! This library auto-generates C headers on build, and Slippi Dolphin is pre-configured
|
|
||||||
//! to locate these headers and link the entire dylib.
|
|
||||||
|
|
||||||
use std::ffi::{c_char, CStr};
|
|
||||||
|
|
||||||
pub mod exi;
|
|
||||||
pub mod game_reporter;
|
|
||||||
pub mod logger;
|
|
||||||
|
|
||||||
/// A small helper method for moving in and out of our known types.
|
|
||||||
pub(crate) fn set<T, F>(instance_ptr: usize, handler: F)
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut T),
|
|
||||||
{
|
|
||||||
// This entire method could possibly be a macro but I'm way too tired
|
|
||||||
// to deal with that syntax right now.
|
|
||||||
|
|
||||||
// Coerce the instance from the pointer. This is theoretically safe since we control
|
|
||||||
// the C++ side and can guarantee that the `instance_ptr` is only owned
|
|
||||||
// by us, and is created/destroyed with the corresponding lifetimes.
|
|
||||||
let mut instance = unsafe { Box::from_raw(instance_ptr as *mut T) };
|
|
||||||
|
|
||||||
handler(&mut instance);
|
|
||||||
|
|
||||||
// Fall back into a raw pointer so Rust doesn't obliterate the object.
|
|
||||||
let _leak = Box::into_raw(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper function for converting c str types to Rust ones with
|
|
||||||
/// some optional args for aiding in debugging should this ever be a problem.
|
|
||||||
///
|
|
||||||
/// This will panic if the strings being passed over cannot be converted, as
|
|
||||||
/// we need the game reporter to be able to run without question. It's also just
|
|
||||||
/// far less verbose than doing this all over the place.
|
|
||||||
pub(crate) fn c_str_to_string(string: *const c_char, fn_label: &str, err_label: &str) -> String {
|
|
||||||
// This is theoretically safe as we control the strings being passed from
|
|
||||||
// the C++ side, and can mostly guarantee that we know what we're getting.
|
|
||||||
let slice = unsafe { CStr::from_ptr(string) };
|
|
||||||
|
|
||||||
match slice.to_str() {
|
|
||||||
Ok(s) => s.to_string(),
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
error = ?e,
|
|
||||||
"[{}] Failed to bridge {}, will panic",
|
|
||||||
fn_label,
|
|
||||||
err_label
|
|
||||||
);
|
|
||||||
|
|
||||||
panic!("Unable to bridge necessary type, panicing");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
46
Externals/SlippiRustExtensions/ffi/src/logger.rs
vendored
46
Externals/SlippiRustExtensions/ffi/src/logger.rs
vendored
|
@ -1,46 +0,0 @@
|
||||||
use std::ffi::{c_char, c_int};
|
|
||||||
|
|
||||||
/// This should be called from the Dolphin LogManager initialization to ensure that
|
|
||||||
/// all logging needs on the Rust side are configured appropriately.
|
|
||||||
///
|
|
||||||
/// For more information, consult `dolphin_logger::init`.
|
|
||||||
///
|
|
||||||
/// Note that `logger_fn` cannot be type-aliased here, otherwise cbindgen will
|
|
||||||
/// mess up the header output. That said, the function type represents:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// void Log(level, log_type, msg);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_logging_init(logger_fn: unsafe extern "C" fn(c_int, c_int, *const c_char)) {
|
|
||||||
dolphin_integrations::ffi::logger::init(logger_fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a log container, which mirrors a Dolphin `LogContainer` (`RustLogContainer`).
|
|
||||||
///
|
|
||||||
/// See `dolphin_logger::register_container` for more information.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_logging_register_container(
|
|
||||||
kind: *const c_char,
|
|
||||||
log_type: c_int,
|
|
||||||
is_enabled: bool,
|
|
||||||
default_log_level: c_int,
|
|
||||||
) {
|
|
||||||
dolphin_integrations::ffi::logger::register_container(kind, log_type, is_enabled, default_log_level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the configuration for a registered logging container.
|
|
||||||
///
|
|
||||||
/// For more information, see `dolphin_logger::update_container`.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_logging_update_container(kind: *const c_char, enabled: bool, level: c_int) {
|
|
||||||
dolphin_integrations::ffi::logger::update_container(kind, enabled, level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the configuration for registered logging container on mainline
|
|
||||||
///
|
|
||||||
/// For more information, see `dolphin_logger::update_container`.
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn slprs_mainline_logging_update_log_level(level: c_int) {
|
|
||||||
dolphin_integrations::ffi::logger::mainline_update_log_level(level);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "slippi-game-reporter"
|
|
||||||
description = "Implements the game reporter service."
|
|
||||||
authors = [
|
|
||||||
"Slippi Team",
|
|
||||||
"Ryan McGrath <ryan@rymc.io>"
|
|
||||||
]
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
ishiiruka = []
|
|
||||||
mainline = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chksum = { version = "0.1.0-rc5", default-features = false, features = ["md5"] }
|
|
||||||
dolphin-integrations = { path = "../dolphin" }
|
|
||||||
flate2 = "1.0"
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
serde_repr = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
ureq = { workspace = true }
|
|
|
@ -1,79 +0,0 @@
|
||||||
//! Implements potential desync ISO detection. The function(s) in this module should typically
|
|
||||||
//! be called from a background thread due to processing time.
|
|
||||||
|
|
||||||
use std::fs::File;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use chksum::prelude::*;
|
|
||||||
|
|
||||||
use dolphin_integrations::{Color, Dolphin, Duration, Log};
|
|
||||||
|
|
||||||
/// ISO hashes that are known to cause problems. We alert the player
|
|
||||||
/// if we detect that they're running one.
|
|
||||||
const KNOWN_DESYNC_ISOS: [&'static str; 4] = [
|
|
||||||
"23d6baef06bd65989585096915da20f2",
|
|
||||||
"27a5668769a54cd3515af47b8d9982f3",
|
|
||||||
"5805fa9f1407aedc8804d0472346fc5f",
|
|
||||||
"9bb3e275e77bb1a160276f2330f93931",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Computes an MD5 hash of the ISO at `iso_path` and writes it back to the value
|
|
||||||
/// behind `iso_hash`.
|
|
||||||
///
|
|
||||||
/// This function is currently more defensive than it probably needs to be, but while
|
|
||||||
/// we move things into Rust I'd like to reduce the chances of anything panic'ing back
|
|
||||||
/// into C++ since that can produce undefined behavior. This just handles every possible
|
|
||||||
/// failure gracefully - however seemingly rare - and simply logs the error.
|
|
||||||
pub fn run(iso_hash: Arc<Mutex<String>>, iso_path: String) {
|
|
||||||
let digest = match File::open(&iso_path) {
|
|
||||||
Ok(mut file) => match file.chksum(HashAlgorithm::MD5) {
|
|
||||||
Ok(digest) => digest,
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Unable to produce ISO MD5 Hash");
|
|
||||||
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Unable to open ISO for MD5 hashing");
|
|
||||||
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let hash = format!("{:x}", digest);
|
|
||||||
|
|
||||||
if !KNOWN_DESYNC_ISOS.contains(&hash.as_str()) {
|
|
||||||
tracing::info!(target: Log::GameReporter, iso_md5_hash = ?hash);
|
|
||||||
} else {
|
|
||||||
// Dump it into the logs as well in case we're ever looking at a user's
|
|
||||||
// logs - may end up being faster than trying to debug with them.
|
|
||||||
tracing::warn!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
iso_md5_hash = ?hash,
|
|
||||||
"Potential desync ISO detected"
|
|
||||||
);
|
|
||||||
|
|
||||||
// This has more line breaks in the C++ version and I frankly do not have the context as to
|
|
||||||
// why they were there - some weird string parsing issue...?
|
|
||||||
//
|
|
||||||
// Settle on 2 (4 before) as a middle ground I guess.
|
|
||||||
Dolphin::add_osd_message(
|
|
||||||
Color::Red,
|
|
||||||
Duration::Custom(20000),
|
|
||||||
"\n\nCAUTION: You are using an ISO that is known to cause desyncs",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
match iso_hash.lock() {
|
|
||||||
Ok(mut iso_hash) => {
|
|
||||||
*iso_hash = hash;
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Unable to lock iso_hash");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
//! This could be rewritten down the road, but the goal is a 1:1 port right now,
|
|
||||||
//! not to rewrite the universe.
|
|
||||||
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::sync::mpsc::{self, Sender};
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use dolphin_integrations::Log;
|
|
||||||
|
|
||||||
mod iso_md5_hasher;
|
|
||||||
|
|
||||||
mod queue;
|
|
||||||
use queue::GameReporterQueue;
|
|
||||||
|
|
||||||
mod types;
|
|
||||||
pub use types::{GameReport, OnlinePlayMode, PlayerReport};
|
|
||||||
|
|
||||||
/// Events that we dispatch into the processing thread.
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub(crate) enum ProcessingEvent {
|
|
||||||
ReportAvailable,
|
|
||||||
Shutdown,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The public interface for the game reporter service. This handles managing any
|
|
||||||
/// necessary background threads and provides hooks for instrumenting the reporting
|
|
||||||
/// process.
|
|
||||||
///
|
|
||||||
/// The inner `GameReporter` is shared between threads and manages field access via
|
|
||||||
/// internal Mutexes. We supply a channel to the processing thread in order to notify
|
|
||||||
/// it of new reports to process.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SlippiGameReporter {
|
|
||||||
iso_md5_hasher_thread: Option<thread::JoinHandle<()>>,
|
|
||||||
processing_thread: Option<thread::JoinHandle<()>>,
|
|
||||||
processing_thread_notifier: Sender<ProcessingEvent>,
|
|
||||||
queue: GameReporterQueue,
|
|
||||||
replay_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlippiGameReporter {
|
|
||||||
/// Initializes and returns a new `SlippiGameReporter`.
|
|
||||||
///
|
|
||||||
/// This spawns and manages a few background threads to handle things like
|
|
||||||
/// report and upload processing, along with checking for troublesome ISOs.
|
|
||||||
/// The core logic surrounding reports themselves lives a layer deeper in `GameReporter`.
|
|
||||||
///
|
|
||||||
/// Currently, failure to spawn any thread should result in a crash - i.e, if we can't
|
|
||||||
/// spawn an OS thread, then there are probably far bigger issues at work here.
|
|
||||||
pub fn new(iso_path: String) -> Self {
|
|
||||||
let queue = GameReporterQueue::new();
|
|
||||||
|
|
||||||
// This is a thread-safe "one time" setter that the MD5 hasher thread
|
|
||||||
// will set when it's done computing.
|
|
||||||
let iso_hash_setter = queue.iso_hash.clone();
|
|
||||||
|
|
||||||
let iso_md5_hasher_thread = thread::Builder::new()
|
|
||||||
.name("SlippiGameReporterISOHasherThread".into())
|
|
||||||
.spawn(move || {
|
|
||||||
iso_md5_hasher::run(iso_hash_setter, iso_path);
|
|
||||||
})
|
|
||||||
.expect("Failed to spawn SlippiGameReporterISOHasherThread.");
|
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
|
||||||
let processing_thread_queue_handle = queue.clone();
|
|
||||||
|
|
||||||
let processing_thread = thread::Builder::new()
|
|
||||||
.name("SlippiGameReporterProcessingThread".into())
|
|
||||||
.spawn(move || {
|
|
||||||
queue::run(processing_thread_queue_handle, receiver);
|
|
||||||
})
|
|
||||||
.expect("Failed to spawn SlippiGameReporterProcessingThread.");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
queue,
|
|
||||||
replay_data: Vec::new(),
|
|
||||||
processing_thread_notifier: sender,
|
|
||||||
processing_thread: Some(processing_thread),
|
|
||||||
iso_md5_hasher_thread: Some(iso_md5_hasher_thread),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Currently unused.
|
|
||||||
pub fn start_new_session(&mut self) {
|
|
||||||
// Maybe we could do stuff here? We used to initialize gameIndex but
|
|
||||||
// that isn't required anymore
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logs replay data that's passed to it.
|
|
||||||
pub fn push_replay_data(&mut self, data: &[u8]) {
|
|
||||||
self.replay_data.extend_from_slice(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a report for processing and signals to the processing thread that there's
|
|
||||||
/// work to be done.
|
|
||||||
///
|
|
||||||
/// Note that when a new report is added, we transfer ownership of all current replay data
|
|
||||||
/// to the game report itself. By doing this, we avoid needing to have a Mutex controlling
|
|
||||||
/// access and pushing replay data as it comes in requires no locking.
|
|
||||||
pub fn log_report(&mut self, mut report: GameReport) {
|
|
||||||
report.replay_data = std::mem::replace(&mut self.replay_data, Vec::new());
|
|
||||||
self.queue.add_report(report);
|
|
||||||
|
|
||||||
if let Err(e) = self.processing_thread_notifier.send(ProcessingEvent::ReportAvailable) {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
error = ?e,
|
|
||||||
"Unable to dispatch ReportAvailable notification"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SlippiGameReporter {
|
|
||||||
type Target = GameReporterQueue;
|
|
||||||
|
|
||||||
/// Support dereferencing to the inner game reporter. This has a "subclass"-like
|
|
||||||
/// effect wherein we don't need to duplicate methods on this layer.
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.queue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for SlippiGameReporter {
|
|
||||||
/// Joins the background threads when we're done, logging if
|
|
||||||
/// any errors are encountered.
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(processing_thread) = self.processing_thread.take() {
|
|
||||||
if let Err(e) = self.processing_thread_notifier.send(ProcessingEvent::Shutdown) {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
error = ?e,
|
|
||||||
"Failed to send shutdown notification to report processing thread, may hang"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = processing_thread.join() {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
error = ?e,
|
|
||||||
"Processing thread failure"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(iso_md5_hasher_thread) = self.iso_md5_hasher_thread.take() {
|
|
||||||
if let Err(e) = iso_md5_hasher_thread.join() {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
error = ?e,
|
|
||||||
"ISO MD5 hasher thread failure"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,419 +0,0 @@
|
||||||
//! This module implements the background queue for the Game Reporter.
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::sync::mpsc::Receiver;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
|
|
||||||
use dolphin_integrations::{Color, Dolphin, Duration as OSDDuration, Log};
|
|
||||||
|
|
||||||
use crate::types::{GameReport, GameReportRequestPayload, OnlinePlayMode};
|
|
||||||
use crate::ProcessingEvent;
|
|
||||||
|
|
||||||
use flate2::write::GzEncoder;
|
|
||||||
use flate2::Compression;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
const GRAPHQL_URL: &str = "https://gql-gateway-dev-dot-slippi.uc.r.appspot.com/graphql";
|
|
||||||
|
|
||||||
/// How many times a report should attempt to send.
|
|
||||||
const MAX_REPORT_ATTEMPTS: i32 = 5;
|
|
||||||
|
|
||||||
/// Expected response payload when saving a report to the server.
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct ReportResponse {
|
|
||||||
success: bool,
|
|
||||||
|
|
||||||
#[serde(rename = "uploadUrl")]
|
|
||||||
upload_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An "inner" struct that holds shared points of data that we need to
|
|
||||||
/// access from multiple threads in this module.
|
|
||||||
///
|
|
||||||
/// By storing this separately, it both somewhat mimics how the original
|
|
||||||
/// C++ class was set up and makes life easier in terms of passing pieces
|
|
||||||
/// of data around various threads.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct GameReporterQueue {
|
|
||||||
pub http_client: ureq::Agent,
|
|
||||||
pub iso_hash: Arc<Mutex<String>>,
|
|
||||||
inner: Arc<Mutex<VecDeque<GameReport>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameReporterQueue {
|
|
||||||
/// Initializes and returns a new game reporter.
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
// We set `max_idle_connections` to `5` to mimic how CURL was configured in
|
|
||||||
// the old C++ version of this module.
|
|
||||||
let http_client = ureq::AgentBuilder::new()
|
|
||||||
.max_idle_connections(5)
|
|
||||||
.user_agent("SlippiGameReporter/Rust v0.1")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
http_client,
|
|
||||||
iso_hash: Arc::new(Mutex::new(String::new())),
|
|
||||||
inner: Arc::new(Mutex::new(VecDeque::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new report to the back of the queue.
|
|
||||||
///
|
|
||||||
/// (The processing thread pulls from the front)
|
|
||||||
pub(crate) fn add_report(&self, report: GameReport) {
|
|
||||||
match self.inner.lock() {
|
|
||||||
Ok(mut lock) => {
|
|
||||||
(*lock).push_back(report);
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
// This should never happen.
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Unable to lock queue, dropping report");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Report a completed match.
|
|
||||||
///
|
|
||||||
/// This doesn't necessarily need to be here, but it's easier to grok the codebase
|
|
||||||
/// if we keep all reporting network calls in one module.
|
|
||||||
pub fn report_completion(&self, uid: String, play_key: String, match_id: String, end_mode: u8) {
|
|
||||||
let mutation = r#"
|
|
||||||
mutation ($report: OnlineGameCompleteInput!) {
|
|
||||||
completeOnlineGame (report: $report)
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let variables = Some(json!({
|
|
||||||
"report": {
|
|
||||||
"matchId": match_id,
|
|
||||||
"fbUid": uid,
|
|
||||||
"playKey": play_key,
|
|
||||||
"endMode": end_mode,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let res = execute_graphql_query(&self.http_client, mutation, variables, Some("completeOnlineGame"));
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(value) if value == "true" => {
|
|
||||||
tracing::info!(target: Log::GameReporter, "Successfully executed completion request")
|
|
||||||
},
|
|
||||||
Ok(value) => tracing::error!(target: Log::GameReporter, ?value, "Error executing completion request",),
|
|
||||||
Err(error) => tracing::error!(target: Log::GameReporter, ?error, "Error executing completion request"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Report an abandoned match.
|
|
||||||
///
|
|
||||||
/// This doesn't necessarily need to be here, but it's easier to grok the codebase
|
|
||||||
/// if we keep all reporting network calls in one module.
|
|
||||||
pub fn report_abandonment(&self, uid: String, play_key: String, match_id: String) {
|
|
||||||
let mutation = r#"
|
|
||||||
mutation ($report: OnlineGameAbandonInput!) {
|
|
||||||
abandonOnlineGame (report: $report)
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let variables = Some(json!({
|
|
||||||
"report": {
|
|
||||||
"matchId": match_id,
|
|
||||||
"fbUid": uid,
|
|
||||||
"playKey": play_key,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
let res = execute_graphql_query(&self.http_client, mutation, variables, Some("abandonOnlineGame"));
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(value) if value == "true" => {
|
|
||||||
tracing::info!(target: Log::GameReporter, "Successfully executed abandonment request")
|
|
||||||
},
|
|
||||||
Ok(value) => tracing::error!(target: Log::GameReporter, ?value, "Error executing abandonment request",),
|
|
||||||
Err(error) => tracing::error!(target: Log::GameReporter, ?error, "Error executing abandonment request"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main loop that processes reports.
|
|
||||||
pub(crate) fn run(reporter: GameReporterQueue, receiver: Receiver<ProcessingEvent>) {
|
|
||||||
loop {
|
|
||||||
// Watch for notification to do work
|
|
||||||
match receiver.recv() {
|
|
||||||
Ok(ProcessingEvent::ReportAvailable) => {
|
|
||||||
process_reports(&reporter, ProcessingEvent::ReportAvailable);
|
|
||||||
},
|
|
||||||
|
|
||||||
Ok(ProcessingEvent::Shutdown) => {
|
|
||||||
tracing::info!(target: Log::GameReporter, "Processing thread winding down");
|
|
||||||
|
|
||||||
process_reports(&reporter, ProcessingEvent::Shutdown);
|
|
||||||
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
|
|
||||||
// This should realistically never happen, since it means the Sender
|
|
||||||
// that's held a level up has been dropped entirely - but we'll log
|
|
||||||
// for the hell of it in case anyone's tweaking the logic.
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
?error,
|
|
||||||
"Failed to receive ProcessingEvent, thread will exit"
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process jobs from the queue.
|
|
||||||
fn process_reports(queue: &GameReporterQueue, event: ProcessingEvent) {
|
|
||||||
let Ok(iso_hash) = queue.iso_hash.lock() else {
|
|
||||||
tracing::warn!(target: Log::GameReporter, "No ISO_HASH available");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(mut report_queue) = queue.inner.lock() else {
|
|
||||||
tracing::warn!(target: Log::GameReporter, "Reporter Queue is dead");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process all reports currently in the queue.
|
|
||||||
while !report_queue.is_empty() {
|
|
||||||
// We only want to pop if we're successful in sending or if we encounter an error
|
|
||||||
// (e.g, max attempts). We pass the locked queue over to work with the borrow checker
|
|
||||||
// here, since otherwise we can't pop without some ugly block work to coerce letting
|
|
||||||
// a mutable borrow drop.
|
|
||||||
match try_send_next_report(&mut *report_queue, event, &queue.http_client, &iso_hash) {
|
|
||||||
Ok(upload_url) => {
|
|
||||||
// Pop the front of the queue. If we have a URL, chuck it all over
|
|
||||||
// to the replay uploader.
|
|
||||||
let report = report_queue.pop_front();
|
|
||||||
|
|
||||||
tracing::info!(target: Log::GameReporter, "Successfully sent report, popping from queue");
|
|
||||||
|
|
||||||
if let (Some(report), Some(upload_url)) = (report, upload_url) {
|
|
||||||
try_upload_replay_data(report.replay_data, upload_url, &queue.http_client);
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::sleep(Duration::ZERO)
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::GameReporter,
|
|
||||||
error = ?error.kind,
|
|
||||||
backoff = ?error.sleep_ms,
|
|
||||||
"Failed to send report"
|
|
||||||
);
|
|
||||||
|
|
||||||
if error.is_last_attempt {
|
|
||||||
tracing::error!(target: Log::GameReporter, "Hit max retry limit, dropping report");
|
|
||||||
let report = report_queue.pop_front(); // Remove the report so it no longer gets processed
|
|
||||||
|
|
||||||
// Tell player their report failed to send
|
|
||||||
if let Some(report) = report {
|
|
||||||
if report.online_mode == OnlinePlayMode::Ranked {
|
|
||||||
Dolphin::add_osd_message(
|
|
||||||
Color::Red,
|
|
||||||
OSDDuration::VeryLong,
|
|
||||||
"Failed to send game report. If you get this often, visit Slippi Discord for help.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::sleep(error.sleep_ms)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The true inner error, minus any metadata.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum ReportSendErrorKind {
|
|
||||||
Net(ureq::Error),
|
|
||||||
JSON(serde_json::Error),
|
|
||||||
GraphQL(String),
|
|
||||||
NotSuccessful(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps errors that can occur during report sending.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ReportSendError {
|
|
||||||
is_last_attempt: bool,
|
|
||||||
sleep_ms: Duration,
|
|
||||||
kind: ReportSendErrorKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a request payload and sends it.
|
|
||||||
///
|
|
||||||
/// If this is successful, it yields back an upload URL endpoint. This can be
|
|
||||||
/// passed to the upload call for processing.
|
|
||||||
fn try_send_next_report(
|
|
||||||
queue: &mut VecDeque<GameReport>,
|
|
||||||
event: ProcessingEvent,
|
|
||||||
http_client: &ureq::Agent,
|
|
||||||
iso_hash: &str,
|
|
||||||
) -> Result<Option<String>, ReportSendError> {
|
|
||||||
let report = (*queue).front_mut().expect("Reporter queue is empty yet it shouldn't be");
|
|
||||||
|
|
||||||
report.attempts += 1;
|
|
||||||
|
|
||||||
// If we're shutting the thread down, limit max attempts to just 1.
|
|
||||||
let max_attempts = match event {
|
|
||||||
ProcessingEvent::Shutdown => 1,
|
|
||||||
_ => MAX_REPORT_ATTEMPTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_last_attempt = report.attempts >= max_attempts;
|
|
||||||
|
|
||||||
let payload = GameReportRequestPayload::with(&report, iso_hash);
|
|
||||||
|
|
||||||
let error_sleep_ms = match is_last_attempt {
|
|
||||||
true => Duration::ZERO,
|
|
||||||
false => Duration::from_millis((report.attempts as u64) * 100),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mutation = r#"
|
|
||||||
mutation ($report: OnlineGameReportInput!) {
|
|
||||||
reportOnlineGame (report: $report) {
|
|
||||||
success
|
|
||||||
uploadUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let variables = Some(json!({
|
|
||||||
"report": payload,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Call execute_graphql_query and get the response body as a String.
|
|
||||||
let response_body =
|
|
||||||
execute_graphql_query(http_client, mutation, variables, Some("reportOnlineGame")).map_err(|e| ReportSendError {
|
|
||||||
is_last_attempt,
|
|
||||||
sleep_ms: error_sleep_ms,
|
|
||||||
kind: e,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Now, parse the response JSON to get the data you need.
|
|
||||||
let response: ReportResponse = serde_json::from_str(&response_body).map_err(|e| ReportSendError {
|
|
||||||
is_last_attempt,
|
|
||||||
sleep_ms: error_sleep_ms,
|
|
||||||
kind: ReportSendErrorKind::JSON(e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !response.success {
|
|
||||||
return Err(ReportSendError {
|
|
||||||
is_last_attempt,
|
|
||||||
sleep_ms: error_sleep_ms,
|
|
||||||
kind: ReportSendErrorKind::NotSuccessful(response_body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(response.upload_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepares and executes a GraphQL query.
|
|
||||||
fn execute_graphql_query(
|
|
||||||
http_client: &ureq::Agent,
|
|
||||||
query: &str,
|
|
||||||
variables: Option<Value>,
|
|
||||||
field: Option<&str>,
|
|
||||||
) -> Result<String, ReportSendErrorKind> {
|
|
||||||
// Prepare the GraphQL request payload
|
|
||||||
let request_body = match variables {
|
|
||||||
Some(vars) => json!({
|
|
||||||
"query": query,
|
|
||||||
"variables": vars,
|
|
||||||
}),
|
|
||||||
None => json!({
|
|
||||||
"query": query,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make the GraphQL request
|
|
||||||
let response = http_client
|
|
||||||
.post(GRAPHQL_URL)
|
|
||||||
.send_json(&request_body)
|
|
||||||
.map_err(ReportSendErrorKind::Net)?;
|
|
||||||
|
|
||||||
// Parse the response JSON
|
|
||||||
let response_json: Value =
|
|
||||||
serde_json::from_str(&response.into_string().unwrap_or_default()).map_err(ReportSendErrorKind::JSON)?;
|
|
||||||
|
|
||||||
// Check for GraphQL errors
|
|
||||||
if let Some(errors) = response_json.get("errors") {
|
|
||||||
if errors.is_array() && !errors.as_array().unwrap().is_empty() {
|
|
||||||
let error_message = serde_json::to_string_pretty(errors).unwrap();
|
|
||||||
return Err(ReportSendErrorKind::GraphQL(error_message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the data response
|
|
||||||
if let Some(data) = response_json.get("data") {
|
|
||||||
let result = match field {
|
|
||||||
Some(field) => data.get(field).unwrap_or(data),
|
|
||||||
None => data,
|
|
||||||
};
|
|
||||||
Ok(result.to_string())
|
|
||||||
} else {
|
|
||||||
Err(ReportSendErrorKind::GraphQL(
|
|
||||||
"No 'data' field in the GraphQL response.".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gzip compresses `input` data to `output` data.
|
|
||||||
fn compress_to_gzip(input: &[u8], output: &mut [u8]) -> Result<usize, std::io::Error> {
|
|
||||||
let mut encoder = GzEncoder::new(output, Compression::default());
|
|
||||||
encoder.write_all(input)?;
|
|
||||||
|
|
||||||
let res = encoder.finish()?;
|
|
||||||
|
|
||||||
Ok(res.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to compress and upload replay data to the url at `upload_url`.
|
|
||||||
fn try_upload_replay_data(data: Vec<u8>, upload_url: String, http_client: &ureq::Agent) {
|
|
||||||
let raw_data_size = data.len() as u32;
|
|
||||||
let rdbs = raw_data_size.to_le_bytes();
|
|
||||||
|
|
||||||
// Add header and footer to replay file
|
|
||||||
let mut contents = vec![
|
|
||||||
b'{', b'U', 3, b'r', b'a', b'w', b'[', b'$', b'U', b'#', b'l', rdbs[3], rdbs[2], rdbs[1], rdbs[0],
|
|
||||||
];
|
|
||||||
contents.extend_from_slice(&data);
|
|
||||||
let mut footer = vec![b'U', 8, b'm', b'e', b't', b'a', b'd', b'a', b't', b'a', b'{', b'}', b'}'];
|
|
||||||
contents.append(&mut footer);
|
|
||||||
|
|
||||||
let mut gzipped_data = vec![0u8; data.len()]; // Resize to some initial size
|
|
||||||
|
|
||||||
let res_size = match compress_to_gzip(&contents, &mut gzipped_data) {
|
|
||||||
Ok(size) => size,
|
|
||||||
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Failed to compress replay");
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
gzipped_data.resize(res_size, 0);
|
|
||||||
|
|
||||||
let response = http_client
|
|
||||||
.put(upload_url.as_str())
|
|
||||||
.set("Content-Type", "application/octet-stream")
|
|
||||||
.set("Content-Encoding", "gzip")
|
|
||||||
.set("X-Goog-Content-Length-Range", "0,10000000")
|
|
||||||
.send_bytes(&gzipped_data);
|
|
||||||
|
|
||||||
if let Err(error) = response {
|
|
||||||
tracing::error!(target: Log::GameReporter, ?error, "Failed to upload replay data",);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/// The different modes that a player could be in.
|
|
||||||
///
|
|
||||||
/// Note that this type uses `serde_repr` to ensure we serialize the value (C-style)
|
|
||||||
/// and not the name itself.
|
|
||||||
#[derive(Copy, Clone, Debug, serde_repr::Serialize_repr, PartialEq, Eq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum OnlinePlayMode {
|
|
||||||
Ranked = 0,
|
|
||||||
Unranked = 1,
|
|
||||||
Direct = 2,
|
|
||||||
Teams = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes metadata about a game that we need to log to the server.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GameReport {
|
|
||||||
pub uid: String,
|
|
||||||
pub play_key: String,
|
|
||||||
pub online_mode: OnlinePlayMode,
|
|
||||||
pub match_id: String,
|
|
||||||
pub attempts: i32,
|
|
||||||
pub duration_frames: u32,
|
|
||||||
pub game_index: u32,
|
|
||||||
pub tie_break_index: u32,
|
|
||||||
pub winner_index: i8,
|
|
||||||
pub game_end_method: u8,
|
|
||||||
pub lras_initiator: i8,
|
|
||||||
pub stage_id: i32,
|
|
||||||
pub players: Vec<PlayerReport>,
|
|
||||||
|
|
||||||
// This is set when we log the report. Anything before then
|
|
||||||
// is a non-allocated `Vec<u8>` to just be a placeholder.
|
|
||||||
pub replay_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Player metadata payload that's logged with game info.
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct PlayerReport {
|
|
||||||
#[serde(rename = "fbUid")]
|
|
||||||
pub uid: String,
|
|
||||||
|
|
||||||
#[serde(rename = "slotType")]
|
|
||||||
pub slot_type: u8,
|
|
||||||
|
|
||||||
#[serde(rename = "damageDone")]
|
|
||||||
pub damage_done: f64,
|
|
||||||
|
|
||||||
#[serde(rename = "stocksRemaining")]
|
|
||||||
pub stocks_remaining: u8,
|
|
||||||
|
|
||||||
#[serde(rename = "characterId")]
|
|
||||||
pub character_id: u8,
|
|
||||||
|
|
||||||
#[serde(rename = "colorId")]
|
|
||||||
pub color_id: u8,
|
|
||||||
|
|
||||||
#[serde(rename = "startingStocks")]
|
|
||||||
pub starting_stocks: i64,
|
|
||||||
|
|
||||||
#[serde(rename = "startingPercent")]
|
|
||||||
pub starting_percent: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The core report payload that's posted to the server.
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct GameReportRequestPayload<'a> {
|
|
||||||
#[serde(rename = "fbUid")]
|
|
||||||
pub uid: &'a str,
|
|
||||||
pub mode: OnlinePlayMode,
|
|
||||||
pub players: &'a [PlayerReport],
|
|
||||||
|
|
||||||
#[serde(rename = "isoHash")]
|
|
||||||
pub iso_hash: &'a str,
|
|
||||||
|
|
||||||
#[serde(rename = "matchId")]
|
|
||||||
pub match_id: &'a str,
|
|
||||||
|
|
||||||
#[serde(rename = "playKey")]
|
|
||||||
pub play_key: &'a str,
|
|
||||||
|
|
||||||
#[serde(rename = "gameDurationFrames")]
|
|
||||||
pub duration_frames: u32,
|
|
||||||
|
|
||||||
#[serde(rename = "gameIndex")]
|
|
||||||
pub game_index: u32,
|
|
||||||
|
|
||||||
#[serde(rename = "tiebreakIndex")]
|
|
||||||
pub tie_break_index: u32,
|
|
||||||
|
|
||||||
#[serde(rename = "winnerIdx")]
|
|
||||||
pub winner_index: i8,
|
|
||||||
|
|
||||||
#[serde(rename = "gameEndMethod")]
|
|
||||||
pub game_end_method: u8,
|
|
||||||
|
|
||||||
#[serde(rename = "lrasInitiator")]
|
|
||||||
pub lras_initiator: i8,
|
|
||||||
|
|
||||||
#[serde(rename = "stageId")]
|
|
||||||
pub stage_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> GameReportRequestPayload<'a> {
|
|
||||||
/// Builds a report request payload that can be serialized for POSTing
|
|
||||||
/// to the server.
|
|
||||||
pub fn with(report: &'a GameReport, iso_hash: &'a str) -> Self {
|
|
||||||
Self {
|
|
||||||
uid: &report.uid,
|
|
||||||
play_key: &report.play_key,
|
|
||||||
iso_hash,
|
|
||||||
players: &report.players,
|
|
||||||
match_id: &report.match_id,
|
|
||||||
mode: report.online_mode,
|
|
||||||
duration_frames: report.duration_frames,
|
|
||||||
game_index: report.game_index,
|
|
||||||
tie_break_index: report.tie_break_index,
|
|
||||||
winner_index: report.winner_index,
|
|
||||||
game_end_method: report.game_end_method,
|
|
||||||
lras_initiator: report.lras_initiator,
|
|
||||||
stage_id: report.stage_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "slippi-jukebox"
|
|
||||||
description = "A built-in solution to play melee's OST in a manner that's effectively detached from emulation."
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = [
|
|
||||||
"Slippi Team",
|
|
||||||
"Daryl Pinto <daryl.j.pinto@gmail.com>"
|
|
||||||
]
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
ishiiruka = []
|
|
||||||
mainline = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.71"
|
|
||||||
dolphin-integrations = { path = "../dolphin" }
|
|
||||||
fastrand = { workspace = true }
|
|
||||||
hps_decode = "0.1.1"
|
|
||||||
process-memory = "0.5.0"
|
|
||||||
rodio = "0.17.1"
|
|
||||||
tracing = { workspace = true }
|
|
40
Externals/SlippiRustExtensions/jukebox/README.md
vendored
40
Externals/SlippiRustExtensions/jukebox/README.md
vendored
|
@ -1,40 +0,0 @@
|
||||||
# Slippi Jukebox
|
|
||||||
|
|
||||||
Slippi Jukebox is a built-in solution to play Melee's OST in a manner that's effectively detached from emulation.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Menu music
|
|
||||||
- All stage music
|
|
||||||
- Alternate stage music<sup>1</sup>
|
|
||||||
- "Versus" splash screen jingle
|
|
||||||
- Ranked stage striking music
|
|
||||||
- Auto-ducking music volume when pausing the game
|
|
||||||
- _Break the Targets_ + _Home Run Contest_ music
|
|
||||||
- Controlling volume with the in-game options and Dolphin's audio volume slider
|
|
||||||
- Lottery menu music
|
|
||||||
- Single player mode<sup>2</sup>
|
|
||||||
|
|
||||||
1. _Alternate stage music has a 12.5% chance of playing. Holding triggers to force alternate tracks to play is not supported._
|
|
||||||
|
|
||||||
2. _In some 1P modes, stage music will differ from vs mode. This behavior is not supported by jukebox. Additionally, the following songs will not play:_
|
|
||||||
<ul>
|
|
||||||
<li>Classic "Stage Clear" Jingle</li>
|
|
||||||
<li>Classic "Continue?" and "Game Over" Jingles</li>
|
|
||||||
<li>Credits music</li>
|
|
||||||
<li>Post-credits congratulations fmv music</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
When a `Jukebox` instance is created (generally from an EXI Device), it scans the iso for music files and stores their locations + file sizes in a hashmap.
|
|
||||||
|
|
||||||
Two child threads are immediately spawned: `JukeboxMessageDispatcher` and `JukeboxMusicPlayer`. Together these threads form an event loop.
|
|
||||||
|
|
||||||
The message dispatcher continuously reads from dolphin's game memory and dispatches relevant events. The music player thread listens to the events and handles them by decoding the appropriate music files and playing them with the default audio device.
|
|
||||||
|
|
||||||
When the `Jukebox` instance is dropped, the child threads are instructed to terminate, the event loop breaks, and the music stops.
|
|
||||||
|
|
||||||
## Decoding Melee's Music
|
|
||||||
|
|
||||||
The logic for decoding Melee's music has been split out into a public library. See the [`hps_decode`](https://crates.io/crates/hps_decode) crate for more. For general information about the `.hps` file format, [see here.](https://github.com/DarylPinto/hps_decode/blob/main/HPS-LAYOUT.md)
|
|
434
Externals/SlippiRustExtensions/jukebox/src/lib.rs
vendored
434
Externals/SlippiRustExtensions/jukebox/src/lib.rs
vendored
|
@ -1,434 +0,0 @@
|
||||||
mod scenes;
|
|
||||||
mod tracks;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use dolphin_integrations::Log;
|
|
||||||
use hps_decode::{hps::Hps, pcm_iterator::PcmIterator};
|
|
||||||
use process_memory::LocalMember;
|
|
||||||
use process_memory::Memory;
|
|
||||||
use rodio::{OutputStream, Sink};
|
|
||||||
use scenes::scene_ids::*;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::ops::ControlFlow::{self, Break, Continue};
|
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
||||||
use std::{thread::sleep, time::Duration};
|
|
||||||
use tracks::TrackId;
|
|
||||||
|
|
||||||
use dolphin_integrations::{Color, Dolphin, Duration as OSDDuration};
|
|
||||||
|
|
||||||
/// Represents a foreign method from the Dolphin side for grabbing the current volume.
|
|
||||||
/// Dolphin represents this as a number from 0 - 100; 0 being mute.
|
|
||||||
pub type ForeignGetVolumeFn = unsafe extern "C" fn() -> std::ffi::c_int;
|
|
||||||
|
|
||||||
const THREAD_LOOP_SLEEP_TIME_MS: u64 = 30;
|
|
||||||
const CHILD_THREAD_COUNT: usize = 2;
|
|
||||||
|
|
||||||
/// By default Slippi Jukebox plays music slightly louder than vanilla melee
|
|
||||||
/// does. This reduces the overall music volume output to 80%. Not totally sure
|
|
||||||
/// if that's the correct amount, but it sounds about right.
|
|
||||||
const VOLUME_REDUCTION_MULTIPLIER: f32 = 0.8;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
struct DolphinGameState {
|
|
||||||
in_game: bool,
|
|
||||||
in_menus: bool,
|
|
||||||
scene_major: u8,
|
|
||||||
scene_minor: u8,
|
|
||||||
stage_id: u8,
|
|
||||||
volume: f32,
|
|
||||||
is_paused: bool,
|
|
||||||
match_info: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DolphinGameState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
in_game: false,
|
|
||||||
in_menus: false,
|
|
||||||
scene_major: SCENE_MAIN_MENU,
|
|
||||||
scene_minor: 0,
|
|
||||||
stage_id: 0,
|
|
||||||
volume: 0.0,
|
|
||||||
is_paused: false,
|
|
||||||
match_info: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum MeleeEvent {
|
|
||||||
TitleScreenEntered,
|
|
||||||
MenuEntered,
|
|
||||||
LotteryEntered,
|
|
||||||
GameStart(u8), // stage id
|
|
||||||
GameEnd,
|
|
||||||
RankedStageStrikeEntered,
|
|
||||||
VsOnlineOpponent,
|
|
||||||
Pause,
|
|
||||||
Unpause,
|
|
||||||
SetVolume(f32),
|
|
||||||
NoOp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum JukeboxEvent {
|
|
||||||
Dropped,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Jukebox {
|
|
||||||
channel_senders: [Sender<JukeboxEvent>; CHILD_THREAD_COUNT],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Jukebox {
|
|
||||||
/// Returns a Jukebox instance that will immediately spawn two child threads
|
|
||||||
/// to try and read game memory and play music. When the returned instance is
|
|
||||||
/// dropped, the child threads will terminate and the music will stop.
|
|
||||||
pub fn new(m_p_ram: *const u8, iso_path: String, get_dolphin_volume_fn: ForeignGetVolumeFn) -> Result<Self> {
|
|
||||||
tracing::info!(target: Log::Jukebox, "Initializing Slippi Jukebox");
|
|
||||||
|
|
||||||
// We are implicitly trusting that these pointers will outlive the jukebox instance
|
|
||||||
let m_p_ram = m_p_ram as usize;
|
|
||||||
let get_dolphin_volume = move || unsafe { get_dolphin_volume_fn() } as f32 / 100.0;
|
|
||||||
|
|
||||||
// This channel is used for the `JukeboxMessageDispatcher` thread to send
|
|
||||||
// messages to the `JukeboxMusicPlayer` thread
|
|
||||||
let (melee_event_tx, melee_event_rx) = channel::<MeleeEvent>();
|
|
||||||
|
|
||||||
// These channels allow the jukebox instance to notify both child
|
|
||||||
// threads when something important happens. Currently its only purpose
|
|
||||||
// is to notify them that the instance is about to be dropped so they
|
|
||||||
// should terminate
|
|
||||||
let (message_dispatcher_thread_tx, message_dispatcher_thread_rx) = channel::<JukeboxEvent>();
|
|
||||||
let (music_thread_tx, music_thread_rx) = channel::<JukeboxEvent>();
|
|
||||||
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("JukeboxMessageDispatcher".to_string())
|
|
||||||
.spawn(move || Self::dispatch_messages(m_p_ram, get_dolphin_volume, message_dispatcher_thread_rx, melee_event_tx))?;
|
|
||||||
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("JukeboxMusicPlayer".to_string())
|
|
||||||
.spawn(move || Self::play_music(m_p_ram, &iso_path, get_dolphin_volume, music_thread_rx, melee_event_rx))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
channel_senders: [message_dispatcher_thread_tx, music_thread_tx],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This thread continuously reads select values from game memory as well
|
|
||||||
/// as the current `volume` value in the dolphin configuration. If it
|
|
||||||
/// notices anything change, it will dispatch a message to the
|
|
||||||
/// `JukeboxMusicPlayer` thread.
|
|
||||||
fn dispatch_messages(
|
|
||||||
m_p_ram: usize,
|
|
||||||
get_dolphin_volume: impl Fn() -> f32,
|
|
||||||
message_dispatcher_thread_rx: Receiver<JukeboxEvent>,
|
|
||||||
melee_event_tx: Sender<MeleeEvent>,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Initial "dolphin state" that will get updated over time
|
|
||||||
let mut prev_state = DolphinGameState::default();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Stop the thread if the jukebox instance will be been dropped
|
|
||||||
if let Ok(event) = message_dispatcher_thread_rx.try_recv() {
|
|
||||||
if matches!(event, JukeboxEvent::Dropped) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continuously check if the dolphin state has changed
|
|
||||||
let state = Self::read_dolphin_game_state(&m_p_ram, get_dolphin_volume())?;
|
|
||||||
|
|
||||||
// If the state has changed,
|
|
||||||
if prev_state != state {
|
|
||||||
// dispatch a message to the music player thread
|
|
||||||
let event = Self::produce_melee_event(&prev_state, &state);
|
|
||||||
tracing::info!(target: Log::Jukebox, "{:?}", event);
|
|
||||||
|
|
||||||
melee_event_tx.send(event)?;
|
|
||||||
prev_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(THREAD_LOOP_SLEEP_TIME_MS));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This thread listens for incoming messages from the
|
|
||||||
/// `JukeboxMessageDispatcher` thread and handles music playback
|
|
||||||
/// accordingly.
|
|
||||||
fn play_music(
|
|
||||||
m_p_ram: usize,
|
|
||||||
iso_path: &str,
|
|
||||||
get_dolphin_volume: impl Fn() -> f32,
|
|
||||||
music_thread_rx: Receiver<JukeboxEvent>,
|
|
||||||
melee_event_rx: Receiver<MeleeEvent>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut iso = std::fs::File::open(iso_path)?;
|
|
||||||
|
|
||||||
tracing::info!(target: Log::Jukebox, "Loading track metadata...");
|
|
||||||
let tracks = utils::create_track_map(&mut iso).map_err(|e| {
|
|
||||||
Dolphin::add_osd_message(
|
|
||||||
Color::Red,
|
|
||||||
OSDDuration::VeryLong,
|
|
||||||
"\nError starting Slippi Jukebox. Your ISO is likely incompatible. Music will not play.",
|
|
||||||
);
|
|
||||||
tracing::error!(target: Log::Jukebox, error = ?e, "Failed to create track map");
|
|
||||||
return e;
|
|
||||||
})?;
|
|
||||||
tracing::info!(target: Log::Jukebox, "Loaded metadata for {} tracks!", tracks.len());
|
|
||||||
|
|
||||||
let (_stream, stream_handle) = OutputStream::try_default()?;
|
|
||||||
let sink = Sink::try_new(&stream_handle)?;
|
|
||||||
|
|
||||||
// The menu track and tournament-mode track are randomly selected
|
|
||||||
// one time, and will be used for the rest of the session
|
|
||||||
let random_menu_tracks = utils::get_random_menu_tracks();
|
|
||||||
|
|
||||||
// Initial music volume and track id. These values will get
|
|
||||||
// updated by the `handle_melee_event` fn whenever a message is
|
|
||||||
// received from the other thread.
|
|
||||||
let initial_state = Self::read_dolphin_game_state(&m_p_ram, get_dolphin_volume())?;
|
|
||||||
let mut volume = initial_state.volume;
|
|
||||||
let mut track_id: Option<TrackId> = None;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if let Some(track_id) = track_id {
|
|
||||||
// Lookup the current track_id in the `tracks` hashmap,
|
|
||||||
// and if it's present, then play it. If not, there will
|
|
||||||
// be silence until a new track_id is set
|
|
||||||
let track = tracks.get(&track_id);
|
|
||||||
if let Some(&(offset, size)) = track {
|
|
||||||
let offset = offset as u64;
|
|
||||||
let size = size as usize;
|
|
||||||
|
|
||||||
// Parse data from the ISO into pcm samples
|
|
||||||
let hps: Hps = utils::read_from_file(&mut iso, offset, size)?
|
|
||||||
.try_into()
|
|
||||||
.with_context(|| format!("The {size} bytes at offset 0x{offset:x?} could not be decoded into an Hps"))?;
|
|
||||||
|
|
||||||
let padding_length = hps.channel_count * hps.sample_rate / 4;
|
|
||||||
let audio_source = HpsAudioSource {
|
|
||||||
pcm: hps.into(),
|
|
||||||
padding_length,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Play song
|
|
||||||
sink.append(audio_source);
|
|
||||||
sink.play();
|
|
||||||
sink.set_volume(volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue to play the song indefinitely while regularly checking
|
|
||||||
// for new messages from the `JukeboxMessageDispatcher` thread
|
|
||||||
loop {
|
|
||||||
// Stop the thread if the jukebox instance will be been dropped
|
|
||||||
if let Ok(event) = music_thread_rx.try_recv() {
|
|
||||||
if matches!(event, JukeboxEvent::Dropped) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we receive an event, handle it. This can include
|
|
||||||
// changing the volume or updating the track and breaking
|
|
||||||
// the inner loop such that the next track starts to play
|
|
||||||
if let Ok(event) = melee_event_rx.try_recv() {
|
|
||||||
if let Break(_) = Self::handle_melee_event(event, &sink, &mut track_id, &mut volume, &random_menu_tracks) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(THREAD_LOOP_SLEEP_TIME_MS));
|
|
||||||
}
|
|
||||||
|
|
||||||
sink.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a events received in the audio playback thread, by changing tracks,
|
|
||||||
/// adjusting volume etc.
|
|
||||||
fn handle_melee_event(
|
|
||||||
event: MeleeEvent,
|
|
||||||
sink: &Sink,
|
|
||||||
track_id: &mut Option<TrackId>,
|
|
||||||
volume: &mut f32,
|
|
||||||
random_menu_tracks: &(TrackId, TrackId),
|
|
||||||
) -> ControlFlow<()> {
|
|
||||||
use self::MeleeEvent::*;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// - Intro movie
|
|
||||||
//
|
|
||||||
// - classic vs screen
|
|
||||||
// - classic victory screen
|
|
||||||
// - classic game over screen
|
|
||||||
// - classic credits
|
|
||||||
// - classic "congratulations movie"
|
|
||||||
// - Adventure mode field intro music
|
|
||||||
|
|
||||||
let (menu_track, tournament_track) = random_menu_tracks;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
TitleScreenEntered | GameEnd => {
|
|
||||||
*track_id = None;
|
|
||||||
},
|
|
||||||
MenuEntered => {
|
|
||||||
*track_id = Some(*menu_track);
|
|
||||||
},
|
|
||||||
LotteryEntered => {
|
|
||||||
*track_id = Some(tracks::TrackId::Lottery);
|
|
||||||
},
|
|
||||||
VsOnlineOpponent => {
|
|
||||||
*track_id = Some(tracks::TrackId::VsOpponent);
|
|
||||||
},
|
|
||||||
RankedStageStrikeEntered => {
|
|
||||||
*track_id = Some(*tournament_track);
|
|
||||||
},
|
|
||||||
GameStart(stage_id) => {
|
|
||||||
*track_id = tracks::get_stage_track_id(stage_id);
|
|
||||||
},
|
|
||||||
Pause => {
|
|
||||||
sink.set_volume(*volume * 0.2);
|
|
||||||
return Continue(());
|
|
||||||
},
|
|
||||||
Unpause => {
|
|
||||||
sink.set_volume(*volume);
|
|
||||||
return Continue(());
|
|
||||||
},
|
|
||||||
SetVolume(received_volume) => {
|
|
||||||
sink.set_volume(received_volume);
|
|
||||||
*volume = received_volume;
|
|
||||||
return Continue(());
|
|
||||||
},
|
|
||||||
NoOp => {
|
|
||||||
return Continue(());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Break(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given the previous dolphin state and current dolphin state, produce an event
|
|
||||||
fn produce_melee_event(prev_state: &DolphinGameState, state: &DolphinGameState) -> MeleeEvent {
|
|
||||||
let vs_screen_1 = state.scene_major == SCENE_VS_ONLINE
|
|
||||||
&& prev_state.scene_minor != SCENE_VS_ONLINE_VERSUS
|
|
||||||
&& state.scene_minor == SCENE_VS_ONLINE_VERSUS;
|
|
||||||
let vs_screen_2 = prev_state.scene_minor == SCENE_VS_ONLINE_VERSUS && state.stage_id == 0;
|
|
||||||
let entered_vs_online_opponent_screen = vs_screen_1 || vs_screen_2;
|
|
||||||
|
|
||||||
if state.scene_major == SCENE_VS_ONLINE
|
|
||||||
&& prev_state.scene_minor != SCENE_VS_ONLINE_RANKED
|
|
||||||
&& state.scene_minor == SCENE_VS_ONLINE_RANKED
|
|
||||||
{
|
|
||||||
MeleeEvent::RankedStageStrikeEntered
|
|
||||||
} else if !prev_state.in_menus && state.in_menus {
|
|
||||||
MeleeEvent::MenuEntered
|
|
||||||
} else if prev_state.scene_major != SCENE_TITLE_SCREEN && state.scene_major == SCENE_TITLE_SCREEN {
|
|
||||||
MeleeEvent::TitleScreenEntered
|
|
||||||
} else if entered_vs_online_opponent_screen {
|
|
||||||
MeleeEvent::VsOnlineOpponent
|
|
||||||
} else if prev_state.scene_major != SCENE_TROPHY_LOTTERY && state.scene_major == SCENE_TROPHY_LOTTERY {
|
|
||||||
MeleeEvent::LotteryEntered
|
|
||||||
} else if (!prev_state.in_game && state.in_game) || prev_state.stage_id != state.stage_id {
|
|
||||||
MeleeEvent::GameStart(state.stage_id)
|
|
||||||
} else if prev_state.in_game && state.in_game && state.match_info == 1 {
|
|
||||||
MeleeEvent::GameEnd
|
|
||||||
} else if prev_state.volume != state.volume {
|
|
||||||
MeleeEvent::SetVolume(state.volume)
|
|
||||||
} else if !prev_state.is_paused && state.is_paused {
|
|
||||||
MeleeEvent::Pause
|
|
||||||
} else if prev_state.is_paused && !state.is_paused {
|
|
||||||
MeleeEvent::Unpause
|
|
||||||
} else {
|
|
||||||
MeleeEvent::NoOp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a `DolphinGameState` by reading Dolphin's memory
|
|
||||||
fn read_dolphin_game_state(m_p_ram: &usize, dolphin_volume_percent: f32) -> Result<DolphinGameState> {
|
|
||||||
#[inline(always)]
|
|
||||||
fn read<T: Copy>(offset: usize) -> Result<T> {
|
|
||||||
Ok(unsafe { LocalMember::<T>::new_offset(vec![offset]).read()? })
|
|
||||||
}
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L8
|
|
||||||
let melee_volume_percent = ((read::<i8>(m_p_ram + 0x45C384)? as f32 - 100.0) * -1.0) / 100.0;
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L16
|
|
||||||
let scene_major = read::<u8>(m_p_ram + 0x479D30)?;
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L19
|
|
||||||
let scene_minor = read::<u8>(m_p_ram + 0x479D33)?;
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L357
|
|
||||||
let stage_id = read::<u8>(m_p_ram + 0x49E753)?;
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L248
|
|
||||||
// 0 = in game, 1 = GAME! screen, 2 = Stage clear in 1p mode? (maybe also victory screen), 3 = menu
|
|
||||||
let match_info = read::<u8>(m_p_ram + 0x46B6A0)?;
|
|
||||||
// https://github.com/bkacjios/m-overlay/blob/d8c629d/source/modules/games/GALE01-2.lua#L353
|
|
||||||
let is_paused = read::<u8>(m_p_ram + 0x4D640F)? == 1;
|
|
||||||
|
|
||||||
Ok(DolphinGameState {
|
|
||||||
in_game: utils::is_in_game(scene_major, scene_minor),
|
|
||||||
in_menus: utils::is_in_menus(scene_major, scene_minor),
|
|
||||||
scene_major,
|
|
||||||
scene_minor,
|
|
||||||
volume: dolphin_volume_percent * melee_volume_percent * VOLUME_REDUCTION_MULTIPLIER,
|
|
||||||
stage_id,
|
|
||||||
is_paused,
|
|
||||||
match_info,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Jukebox {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
tracing::info!(target: Log::Jukebox, "Dropping Slippi Jukebox");
|
|
||||||
for sender in &self.channel_senders {
|
|
||||||
if let Err(e) = sender.send(JukeboxEvent::Dropped) {
|
|
||||||
tracing::error!(
|
|
||||||
target: Log::Jukebox,
|
|
||||||
"Failed to notify child thread that Jukebox is dropping: {e}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This wrapper allows us to implement `rodio::Source`
|
|
||||||
struct HpsAudioSource {
|
|
||||||
pcm: PcmIterator,
|
|
||||||
padding_length: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iterator for HpsAudioSource {
|
|
||||||
type Item = i16;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
// We need to pad the start of the music playback with a quarter second
|
|
||||||
// of silence so when two tracks are loaded in quick succession, we
|
|
||||||
// don't hear a quick "blip" from the first track. This happens in
|
|
||||||
// practice because scene_minor tells us we're in-game before stage_id
|
|
||||||
// has a chance to update from the previously played stage.
|
|
||||||
//
|
|
||||||
// Return 0s (silence) for the length of the padding
|
|
||||||
if self.padding_length > 0 {
|
|
||||||
self.padding_length -= 1;
|
|
||||||
return Some(0);
|
|
||||||
}
|
|
||||||
// Then start iterating on the actual samples
|
|
||||||
self.pcm.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rodio::Source for HpsAudioSource {
|
|
||||||
fn current_frame_len(&self) -> Option<usize> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn channels(&self) -> u16 {
|
|
||||||
self.pcm.channel_count as u16
|
|
||||||
}
|
|
||||||
fn sample_rate(&self) -> u32 {
|
|
||||||
self.pcm.sample_rate
|
|
||||||
}
|
|
||||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
396
Externals/SlippiRustExtensions/jukebox/src/scenes.rs
vendored
396
Externals/SlippiRustExtensions/jukebox/src/scenes.rs
vendored
|
@ -1,396 +0,0 @@
|
||||||
/// Sourced from M'OVerlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) mod scene_ids {
|
|
||||||
pub(crate) const MATCH_NO_RESULT: u8 = 0x00;
|
|
||||||
pub(crate) const MATCH_GAME: u8 = 0x02;
|
|
||||||
pub(crate) const MATCH_STAGE_CLEAR: u8 = 0x03;
|
|
||||||
pub(crate) const MATCH_STAGE_FAILURE: u8 = 0x04;
|
|
||||||
pub(crate) const MATCH_STAGE_CLEAR3: u8 = 0x05;
|
|
||||||
pub(crate) const MATCH_NEW_RECORD: u8 = 0x06;
|
|
||||||
pub(crate) const MATCH_NO_CONTEST: u8 = 0x07;
|
|
||||||
pub(crate) const MATCH_RETRY: u8 = 0x08;
|
|
||||||
pub(crate) const MATCH_GAME_CLEAR: u8 = 0x09;
|
|
||||||
|
|
||||||
// MAJOR FLAGS
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN: u8 = 0x00;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_MAIN_MENU: u8 = 0x01;
|
|
||||||
// MENU FLAGS
|
|
||||||
pub(crate) const MENU_MAIN: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_MAIN_1P: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_MAIN_VS: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_MAIN_TROPHY: u8 = 0x02;
|
|
||||||
pub(crate) const SELECT_MAIN_OPTIONS: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_MAIN_DATA: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_1P: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_1P_REGULAR: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_1P_EVENT: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_1P_ONLINE: u8 = 0x2;
|
|
||||||
pub(crate) const SELECT_1P_STADIUM: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_1P_TRAINING: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_VS: u8 = 0x02;
|
|
||||||
pub(crate) const SELECT_VS_MELEE: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_VS_TOURNAMENT: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_VS_SPECIAL: u8 = 0x02;
|
|
||||||
pub(crate) const SELECT_VS_CUSTOM: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_VS_NAMEENTRY: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_TROPHIES: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_TROPHIES_GALLERY: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_TROPHIES_LOTTERY: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_TROPHIES_COLLECTION: u8 = 0x02;
|
|
||||||
|
|
||||||
pub(crate) const MENU_OPTIONS: u8 = 0x04;
|
|
||||||
pub(crate) const SELECT_OPTIONS_RUMBLE: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_OPTIONS_SOUND: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_OPTIONS_DISPLAY: u8 = 0x02;
|
|
||||||
pub(crate) const SELECT_OPTIONS_UNKNOWN: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_OPTIONS_LANGUAGE: u8 = 0x04;
|
|
||||||
pub(crate) const SELECT_OPTIONS_ERASE_DATA: u8 = 0x05;
|
|
||||||
|
|
||||||
pub(crate) const MENU_ONLINE: u8 = 0x08;
|
|
||||||
pub(crate) const SELECT_ONLINE_RANKED: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_ONLINE_UNRANKED: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_ONLINE_DIRECT: u8 = 0x02;
|
|
||||||
pub(crate) const SELECT_ONLINE_TEAMS: u8 = 0x03;
|
|
||||||
pub(crate) const SELECT_ONLINE_LOGOUT: u8 = 0x05;
|
|
||||||
|
|
||||||
pub(crate) const MENU_STADIUM: u8 = 0x09;
|
|
||||||
pub(crate) const SELECT_STADIUM_TARGET_TEST: u8 = 0x00;
|
|
||||||
pub(crate) const SELECT_STADIUM_HOMERUN_CONTEST: u8 = 0x01;
|
|
||||||
pub(crate) const SELECT_STADIUM_MULTIMAN_MELEE: u8 = 0x02;
|
|
||||||
|
|
||||||
pub(crate) const MENU_RUMBLE: u8 = 0x13;
|
|
||||||
pub(crate) const MENU_SOUND: u8 = 0x14;
|
|
||||||
pub(crate) const MENU_DISPLAY: u8 = 0x15;
|
|
||||||
pub(crate) const MENU_UNKNOWN1: u8 = 0x16;
|
|
||||||
pub(crate) const MENU_LANGUAGE: u8 = 0x17;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_VS_MODE: u8 = 0x02;
|
|
||||||
// MINOR FLAGS
|
|
||||||
pub(crate) const SCENE_VS_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_VS_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_VS_INGAME: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_VS_POSTGAME: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE: u8 = 0x03;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_1_VS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_1: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_2_VS: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_2: u8 = 0x03;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_3_VS: u8 = 0x04;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_3: u8 = 0x05;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_4_VS: u8 = 0x06;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_4: u8 = 0x07;
|
|
||||||
// pub(crate) const SCENE_CLASSIC_LEVEL_5_VS: u8 = 0x08;
|
|
||||||
// pub(crate) const SCENE_CLASSIC_LEVEL_5: u8 = 0x09;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_5_VS: u8 = 0x10;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_5: u8 = 0x09;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_16: u8 = 0x20;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_16_VS: u8 = 0x21;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_24: u8 = 0x30;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_24_VS: u8 = 0x31;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_BREAK_THE_TARGETS_INTRO: u8 = 0x16;
|
|
||||||
pub(crate) const SCENE_CLASSIC_BREAK_THE_TARGETS: u8 = 0x17;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_TROPHY_STAGE_INTRO: u8 = 0x28;
|
|
||||||
pub(crate) const SCENE_CLASSIC_TROPHY_STAGE_TARGETS: u8 = 0x29;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_RACE_TO_FINISH_INTRO: u8 = 0x40;
|
|
||||||
pub(crate) const SCENE_CLASSIC_RACE_TO_FINISH_TARGETS: u8 = 0x41;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_56: u8 = 0x38;
|
|
||||||
pub(crate) const SCENE_CLASSIC_LEVEL_56_VS: u8 = 0x39;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_MASTER_HAND: u8 = 0x51;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_CONTINUE: u8 = 0x69;
|
|
||||||
pub(crate) const SCENE_CLASSIC_CSS: u8 = 0x70;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_INTRO: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_LUIGI: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KINGDOM_BATTLE: u8 = 0x03;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_INTRO: u8 = 0x08;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_TINY_BATTLE: u8 = 0x09;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_GIANT_BATTLE: u8 = 0x0A;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_UNDERGROUND_MAZE_INTRO: u8 = 0x10;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_UNDERGROUND_MAZE: u8 = 0x11;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_HYRULE_TEMPLE_BATTLE: u8 = 0x12;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BRINSTAR_INTRO: u8 = 0x18;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BRINSTAR: u8 = 0x19;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES_INTRO: u8 = 0x1A;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES: u8 = 0x1B;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ESCAPE_ZEBES_ESCAPE: u8 = 0x1C;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_INTRO: u8 = 0x20;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_BATTLE: u8 = 0x21;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_INTRO: u8 = 0x22;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_BATTLE: u8 = 0x23;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_INTRO: u8 = 0x24;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_BATTLE: u8 = 0x25;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CORNERIA_INTRO: u8 = 0x28;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_1: u8 = 0x29;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CORNERIA_RAID: u8 = 0x2A;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_2: u8 = 0x2B;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CORNERIA_BATTLE_3: u8 = 0x2C;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_POKEMON_STADIUM_INTRO: u8 = 0x30;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_POKEMON_STADIUM_BATTLE: u8 = 0x31;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_CARS: u8 = 0x38;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_INTRO: u8 = 0x39;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_RACE: u8 = 0x3A;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FZERO_GRAND_PRIX_BATTLE: u8 = 0x3B;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ONETT_INTRO: u8 = 0x40;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ONETT_BATTLE: u8 = 0x41;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ICICLE_MOUNTAIN_INTRO: u8 = 0x48;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_ICICLE_MOUNTAIN_CLIMB: u8 = 0x49;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_INTRO: u8 = 0x50;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_BATTLE: u8 = 0x51;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_METAL_INTRO: u8 = 0x52;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_BATTLEFIELD_METAL_BATTLE: u8 = 0x53;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_INTRO: u8 = 0x58;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_BATTLE: u8 = 0x59;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_POSE: u8 = 0x5A;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_FINAL_DESTINATION_WINNER: u8 = 0x5B;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_CSS: u8 = 0x70;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ALL_STAR_MODE: u8 = 0x05;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_1: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_1: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_2: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_2: u8 = 0x03;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_3: u8 = 0x04;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_3: u8 = 0x05;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_4: u8 = 0x06;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_4: u8 = 0x07;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_5: u8 = 0x08;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_5: u8 = 0x09;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_6: u8 = 0x10;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_6: u8 = 0x11;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_7: u8 = 0x12;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_7: u8 = 0x13;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_8: u8 = 0x14;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_8: u8 = 0x15;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_9: u8 = 0x16;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_9: u8 = 0x17;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_10: u8 = 0x18;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_10: u8 = 0x19;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_11: u8 = 0x20;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_11: u8 = 0x21;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_12: u8 = 0x22;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_12: u8 = 0x23;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_13: u8 = 0x24;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_13: u8 = 0x25;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_14: u8 = 0x26;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_14: u8 = 0x27;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_15: u8 = 0x28;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_15: u8 = 0x29;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_16: u8 = 0x30;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_16: u8 = 0x31;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_17: u8 = 0x32;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_17: u8 = 0x33;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_18: u8 = 0x34;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_18: u8 = 0x35;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_19: u8 = 0x36;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_19: u8 = 0x37;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_20: u8 = 0x38;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_20: u8 = 0x39;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_21: u8 = 0x40;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_21: u8 = 0x41;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_22: u8 = 0x42;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_22: u8 = 0x43;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_23: u8 = 0x44;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_23: u8 = 0x45;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_24: u8 = 0x46;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_24: u8 = 0x47;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_25: u8 = 0x48;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_25: u8 = 0x49;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_26: u8 = 0x50;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_26: u8 = 0x51;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_27: u8 = 0x52;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_28: u8 = 0x53;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_29: u8 = 0x54;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_29: u8 = 0x55;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_30: u8 = 0x56;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_30: u8 = 0x57;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_31: u8 = 0x58;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_31: u8 = 0x59;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_LEVEL_32: u8 = 0x60;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_REST_AREA_32: u8 = 0x61;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_CSS: u8 = 0x70;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_DEBUG: u8 = 0x06;
|
|
||||||
pub(crate) const SCENE_SOUND_TEST: u8 = 0x07;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_VS_ONLINE: u8 = 0x08; // SLIPPI ONLINE
|
|
||||||
pub(crate) const SCENE_VS_ONLINE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_VS_ONLINE_SSS: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_VS_ONLINE_INGAME: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_VS_ONLINE_VERSUS: u8 = 0x04;
|
|
||||||
pub(crate) const SCENE_VS_ONLINE_RANKED: u8 = 0x05;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_UNKOWN_1: u8 = 0x09;
|
|
||||||
pub(crate) const SCENE_CAMERA_MODE: u8 = 0x0A;
|
|
||||||
pub(crate) const SCENE_TROPHY_GALLERY: u8 = 0x0B;
|
|
||||||
pub(crate) const SCENE_TROPHY_LOTTERY: u8 = 0x0C;
|
|
||||||
pub(crate) const SCENE_TROPHY_COLLECTION: u8 = 0x0D;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_START_MATCH: u8 = 0x0E; // Slippi Replays
|
|
||||||
pub(crate) const SCENE_START_MATCH_INGAME: u8 = 0x01; // Set when the replay is actually playing out
|
|
||||||
pub(crate) const SCENE_START_MATCH_UNKNOWN: u8 = 0x03; // Seems to be set right before the match loads
|
|
||||||
|
|
||||||
pub(crate) const SCENE_TARGET_TEST: u8 = 0x0F;
|
|
||||||
pub(crate) const SCENE_TARGET_TEST_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_TARGET_TEST_INGAME: u8 = 0x1;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_SUPER_SUDDEN_DEATH: u8 = 0x10;
|
|
||||||
pub(crate) const SCENE_SSD_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_SSD_SSS: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_SSD_INGAME: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_SSD_POSTGAME: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_INVISIBLE_MELEE: u8 = 0x11;
|
|
||||||
pub(crate) const MENU_INVISIBLE_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const MENU_INVISIBLE_MELEE_SSS: u8 = 0x01;
|
|
||||||
pub(crate) const MENU_INVISIBLE_MELEE_INGAME: u8 = 0x02;
|
|
||||||
pub(crate) const MENU_INVISIBLE_MELEE_POSTGAME: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_SLOW_MO_MELEE: u8 = 0x12;
|
|
||||||
pub(crate) const MENU_SLOW_MO_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const MENU_SLOW_MO_MELEE_SSS: u8 = 0x01;
|
|
||||||
pub(crate) const MENU_SLOW_MO_MELEE_INGAME: u8 = 0x02;
|
|
||||||
pub(crate) const MENU_SLOW_MO_MELEE_POSTGAME: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const MENU_LIGHTNING_MELEE: u8 = 0x13;
|
|
||||||
pub(crate) const MENU_LIGHTNING_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const MENU_LIGHTNING_MELEE_SSS: u8 = 0x01;
|
|
||||||
pub(crate) const MENU_LIGHTNING_MELEE_INGAME: u8 = 0x02;
|
|
||||||
pub(crate) const MENU_LIGHTNING_MELEE_POSTGAME: u8 = 0x04;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CHARACTER_APPROACHING: u8 = 0x14;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE_COMPLETE: u8 = 0x15;
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE_TROPHY: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE_CREDITS: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE_CHARACTER_VIDEO: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_CLASSIC_MODE_CONGRATS: u8 = 0x03;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_COMPLETE: u8 = 0x16;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_TROPHY: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_CREDITS: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_CHARACTER_VIDEO: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_CONGRATS: u8 = 0x03;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ALL_STAR_COMPLETE: u8 = 0x17;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_TROPHY: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_CREDITS: u8 = 0x01;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_CHARACTER_VIDEO: u8 = 0x02;
|
|
||||||
pub(crate) const SCENE_ALL_STAR_CONGRATS: u8 = 0x03;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE: u8 = 0x18;
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE_INTRO_VIDEO: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE_FIGHT_1: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE_BETWEEN_FIGHTS: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE_FIGHT_2: u8 = 0x3;
|
|
||||||
pub(crate) const SCENE_TITLE_SCREEN_IDLE_HOW_TO_PLAY: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ADVENTURE_MODE_CINEMEATIC: u8 = 0x19;
|
|
||||||
pub(crate) const SCENE_CHARACTER_UNLOCKED: u8 = 0x1A;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_TOURNAMENT: u8 = 0x1B;
|
|
||||||
pub(crate) const SCENE_TOURNAMENT_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_TOURNAMENT_BRACKET: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_TOURNAMENT_INGAME: u8 = 0x4;
|
|
||||||
pub(crate) const SCENE_TOURNAMENT_POSTGAME: u8 = 0x6;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_TRAINING_MODE: u8 = 0x1C;
|
|
||||||
pub(crate) const SCENE_TRAINING_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_TRAINING_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_TRAINING_INGAME: u8 = 0x2;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_TINY_MELEE: u8 = 0x1D;
|
|
||||||
pub(crate) const SCENE_TINY_MELEE_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_TINY_MELEE_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_TINY_MELEE_INGAME: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_TINY_MELEE_POSTGAME: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_GIANT_MELEE: u8 = 0x1E;
|
|
||||||
pub(crate) const SCENE_GIANT_MELEE_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_GIANT_MELEE_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_GIANT_MELEE_INGAME: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_GIANT_MELEE_POSTGAME: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_STAMINA_MODE: u8 = 0x1F;
|
|
||||||
pub(crate) const SCENE_STAMINA_MODE_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_STAMINA_MODE_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_STAMINA_MODE_INGAME: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_STAMINA_MODE_POSTGAME: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_HOME_RUN_CONTEST: u8 = 0x20;
|
|
||||||
pub(crate) const SCENE_HOME_RUN_CONTEST_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_HOME_RUN_CONTEST_INGAME: u8 = 0x1;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_10_MAN_MELEE: u8 = 0x21;
|
|
||||||
pub(crate) const SCENE_10_MAN_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_10_MAN_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_100_MAN_MELEE: u8 = 0x22;
|
|
||||||
pub(crate) const SCENE_100_MAN_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_100_MAN_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_3_MINUTE_MELEE: u8 = 0x23;
|
|
||||||
pub(crate) const SCENE_3_MINUTE_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_3_MINUTE_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_15_MINUTE_MELEE: u8 = 0x24;
|
|
||||||
pub(crate) const SCENE_15_MINUTE_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_15_MINUTE_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_ENDLESS_MELEE: u8 = 0x25;
|
|
||||||
pub(crate) const SCENE_ENDLESS_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_ENDLESS_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_CRUEL_MELEE: u8 = 0x26;
|
|
||||||
pub(crate) const SCENE_CRUEL_MELEE_CSS: u8 = 0x00;
|
|
||||||
pub(crate) const SCENE_CRUEL_MELEE_INGAME: u8 = 0x01;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_PROGRESSIVE_SCAN: u8 = 0x27;
|
|
||||||
pub(crate) const SCENE_PLAY_INTRO_VIDEO: u8 = 0x28;
|
|
||||||
pub(crate) const SCENE_MEMORY_CARD_OVERWRITE: u8 = 0x29;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_FIXED_CAMERA_MODE: u8 = 0x2A;
|
|
||||||
pub(crate) const SCENE_FIXED_CAMERA_MODE_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_FIXED_CAMERA_MODE_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_FIXED_CAMERA_MODE_INGAME: u8 = 0x2;
|
|
||||||
pub(crate) const SCENE_FIXED_CAMERA_MODE_POSTGAME: u8 = 0x4;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_EVENT_MATCH: u8 = 0x2B;
|
|
||||||
pub(crate) const SCENE_EVENT_MATCH_SELECT: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_EVENT_MATCH_INGAME: u8 = 0x1;
|
|
||||||
|
|
||||||
pub(crate) const SCENE_SINGLE_BUTTON_MODE: u8 = 0x2C;
|
|
||||||
pub(crate) const SCENE_SINGLE_BUTTON_MODE_CSS: u8 = 0x0;
|
|
||||||
pub(crate) const SCENE_SINGLE_BUTTON_MODE_SSS: u8 = 0x1;
|
|
||||||
pub(crate) const SCENE_SINGLE_BUTTON_MODE_INGAME: u8 = 0x2;
|
|
||||||
|
|
||||||
}
|
|
171
Externals/SlippiRustExtensions/jukebox/src/tracks.rs
vendored
171
Externals/SlippiRustExtensions/jukebox/src/tracks.rs
vendored
|
@ -1,171 +0,0 @@
|
||||||
/// IDs for all the songs that Slippi Jukebox can play. Any track that
|
|
||||||
/// exists in vanilla can be added
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub(crate) enum TrackId {
|
|
||||||
Menu1,
|
|
||||||
Menu2,
|
|
||||||
Lottery,
|
|
||||||
TournamentMode1,
|
|
||||||
TournamentMode2,
|
|
||||||
VsOpponent,
|
|
||||||
PeachsCastle,
|
|
||||||
RainbowCruise,
|
|
||||||
KongoJungle,
|
|
||||||
JungleJapes,
|
|
||||||
GreatBay,
|
|
||||||
Saria,
|
|
||||||
Temple,
|
|
||||||
FireEmblem,
|
|
||||||
Brinstar,
|
|
||||||
BrinstarDepths,
|
|
||||||
YoshisStory,
|
|
||||||
YoshisIsland,
|
|
||||||
SuperMario3,
|
|
||||||
FountainOfDreams,
|
|
||||||
GreenGreens,
|
|
||||||
Corneria,
|
|
||||||
Venom,
|
|
||||||
PokemonStadium,
|
|
||||||
PokemonBattle,
|
|
||||||
Pokefloats,
|
|
||||||
MuteCity,
|
|
||||||
BigBlue,
|
|
||||||
MachRider,
|
|
||||||
Onett,
|
|
||||||
Mother2,
|
|
||||||
Fourside,
|
|
||||||
IcicleMountain,
|
|
||||||
BalloonFighter,
|
|
||||||
MushroomKingdom,
|
|
||||||
DrMario,
|
|
||||||
MushroomKingdomII,
|
|
||||||
FlatZone,
|
|
||||||
DreamLand64,
|
|
||||||
YoshisIsland64,
|
|
||||||
KongoJungle64,
|
|
||||||
Battlefield,
|
|
||||||
MultimanMelee,
|
|
||||||
FinalDestination,
|
|
||||||
MultimanMelee2,
|
|
||||||
BreakTheTargets,
|
|
||||||
BrinstarEscape,
|
|
||||||
AllStarRestArea,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given the filename of a melee music track, return the associated track ID
|
|
||||||
pub(crate) fn get_track_id_by_filename(track_filename: &str) -> Option<TrackId> {
|
|
||||||
use self::TrackId::*;
|
|
||||||
|
|
||||||
match track_filename {
|
|
||||||
"menu01.hps" => Some(Menu1),
|
|
||||||
"menu3.hps" => Some(Menu2),
|
|
||||||
"menu02.hps" => Some(Lottery),
|
|
||||||
"vs_hyou1.hps" => Some(TournamentMode1),
|
|
||||||
"vs_hyou2.hps" => Some(TournamentMode2),
|
|
||||||
"intro_es.hps" => Some(VsOpponent),
|
|
||||||
"castle.hps" => Some(PeachsCastle),
|
|
||||||
"rcruise.hps" => Some(RainbowCruise),
|
|
||||||
"garden.hps" => Some(KongoJungle),
|
|
||||||
"kongo.hps" => Some(JungleJapes),
|
|
||||||
"greatbay.hps" => Some(GreatBay),
|
|
||||||
"saria.hps" => Some(Saria),
|
|
||||||
"shrine.hps" => Some(Temple),
|
|
||||||
"akaneia.hps" => Some(FireEmblem),
|
|
||||||
"zebes.hps" => Some(Brinstar),
|
|
||||||
"kraid.hps" => Some(BrinstarDepths),
|
|
||||||
"ystory.hps" => Some(YoshisStory),
|
|
||||||
"yorster.hps" => Some(YoshisIsland),
|
|
||||||
"smari3.hps" => Some(SuperMario3),
|
|
||||||
"izumi.hps" => Some(FountainOfDreams),
|
|
||||||
"greens.hps" => Some(GreenGreens),
|
|
||||||
"corneria.hps" => Some(Corneria),
|
|
||||||
"venom.hps" => Some(Venom),
|
|
||||||
"pstadium.hps" => Some(PokemonStadium),
|
|
||||||
"pokesta.hps" => Some(PokemonBattle),
|
|
||||||
"pura.hps" => Some(Pokefloats),
|
|
||||||
"mutecity.hps" => Some(MuteCity),
|
|
||||||
"bigblue.hps" => Some(BigBlue),
|
|
||||||
"mrider.hps" => Some(MachRider),
|
|
||||||
"onetto.hps" => Some(Onett),
|
|
||||||
"onetto2.hps" => Some(Mother2),
|
|
||||||
"fourside.hps" => Some(Fourside),
|
|
||||||
"icemt.hps" => Some(IcicleMountain),
|
|
||||||
"baloon.hps" => Some(BalloonFighter),
|
|
||||||
"inis1_01.hps" => Some(MushroomKingdom),
|
|
||||||
"docmari.hps" => Some(DrMario),
|
|
||||||
"inis2_01.hps" => Some(MushroomKingdomII),
|
|
||||||
"flatzone.hps" => Some(FlatZone),
|
|
||||||
"old_kb.hps" => Some(DreamLand64),
|
|
||||||
"old_ys.hps" => Some(YoshisIsland64),
|
|
||||||
"old_dk.hps" => Some(KongoJungle64),
|
|
||||||
"sp_zako.hps" => Some(Battlefield),
|
|
||||||
"hyaku.hps" => Some(MultimanMelee),
|
|
||||||
"sp_end.hps" => Some(FinalDestination),
|
|
||||||
"hyaku2.hps" => Some(MultimanMelee2),
|
|
||||||
"target.hps" => Some(BreakTheTargets),
|
|
||||||
"siren.hps" => Some(BrinstarEscape),
|
|
||||||
"1p_qk.hps" => Some(AllStarRestArea),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a stage ID, retrieve the ID of the track that should play
|
|
||||||
pub(crate) fn get_stage_track_id(stage_id: u8) -> Option<TrackId> {
|
|
||||||
use self::TrackId::*;
|
|
||||||
|
|
||||||
// Stage IDs and their associated tracks
|
|
||||||
let track_ids: Option<(TrackId, Option<TrackId>)> = match stage_id {
|
|
||||||
0x02 | 0x1F => Some((PeachsCastle, None)),
|
|
||||||
0x03 => Some((RainbowCruise, None)),
|
|
||||||
0x04 => Some((KongoJungle, None)),
|
|
||||||
0x05 => Some((JungleJapes, None)),
|
|
||||||
0x06 => Some((GreatBay, Some(Saria))),
|
|
||||||
0x07 => Some((Temple, Some(FireEmblem))),
|
|
||||||
0x08 => Some((Brinstar, None)),
|
|
||||||
0x09 => Some((BrinstarDepths, None)),
|
|
||||||
0x0A => Some((YoshisStory, None)),
|
|
||||||
0x0B => Some((YoshisIsland, Some(SuperMario3))),
|
|
||||||
0x0C => Some((FountainOfDreams, None)),
|
|
||||||
0x0D => Some((GreenGreens, None)),
|
|
||||||
0x0E => Some((Corneria, None)),
|
|
||||||
0x0F => Some((Venom, None)),
|
|
||||||
0x10 => Some((PokemonStadium, Some(PokemonBattle))),
|
|
||||||
0x11 => Some((Pokefloats, None)),
|
|
||||||
0x12 => Some((MuteCity, None)),
|
|
||||||
0x13 => Some((BigBlue, Some(MachRider))),
|
|
||||||
0x14 => Some((Onett, Some(Mother2))),
|
|
||||||
0x15 => Some((Fourside, None)),
|
|
||||||
0x16 => Some((IcicleMountain, Some(BalloonFighter))),
|
|
||||||
0x18 => Some((MushroomKingdom, Some(DrMario))),
|
|
||||||
0x19 => Some((MushroomKingdomII, Some(DrMario))),
|
|
||||||
0x1B => Some((FlatZone, None)),
|
|
||||||
0x1C => Some((DreamLand64, None)),
|
|
||||||
0x1D => Some((YoshisIsland64, None)),
|
|
||||||
0x1E => Some((KongoJungle64, None)),
|
|
||||||
0x24 => Some((Battlefield, Some(MultimanMelee))),
|
|
||||||
0x25 => Some((FinalDestination, Some(MultimanMelee2))),
|
|
||||||
// Snag trophies
|
|
||||||
0x26 => Some((Lottery, None)),
|
|
||||||
// Race to the Finish
|
|
||||||
0x27 => Some((Battlefield, None)),
|
|
||||||
// Adventure Mode Field Stages
|
|
||||||
0x20 => Some((Temple, None)),
|
|
||||||
0x21 => Some((BrinstarEscape, None)),
|
|
||||||
0x22 => Some((BigBlue, None)),
|
|
||||||
// All-Star Rest Area
|
|
||||||
0x42 => Some((AllStarRestArea, None)),
|
|
||||||
// Break the Targets + Home Run Contest
|
|
||||||
0x2C | 0x28 | 0x43 | 0x33 | 0x31 | 0x37 | 0x3D | 0x2B | 0x29 | 0x41 | 0x2D | 0x2E | 0x36 | 0x2F | 0x30 | 0x3B | 0x3E
|
|
||||||
| 0x32 | 0x2A | 0x38 | 0x39 | 0x3A | 0x35 | 0x3F | 0x34 | 0x40 => Some((BreakTheTargets, None)),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the stage has an alternate track associated, there's a 12.5% chance it will be selected
|
|
||||||
match track_ids {
|
|
||||||
Some(track_ids) => match track_ids {
|
|
||||||
(_, Some(id)) if fastrand::u8(0..8) == 0 => Some(id),
|
|
||||||
(id, _) => Some(id),
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
206
Externals/SlippiRustExtensions/jukebox/src/utils.rs
vendored
206
Externals/SlippiRustExtensions/jukebox/src/utils.rs
vendored
|
@ -1,206 +0,0 @@
|
||||||
use crate::scenes::scene_ids::*;
|
|
||||||
use crate::tracks::{get_track_id_by_filename, TrackId};
|
|
||||||
use crate::Result;
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::ffi::CStr;
|
|
||||||
use std::io::{Read, Seek};
|
|
||||||
|
|
||||||
/// Get an unsigned 24 bit integer from a byte slice
|
|
||||||
fn read_u24(bytes: &[u8], offset: usize) -> u32 {
|
|
||||||
let size = 3;
|
|
||||||
let end = offset + size;
|
|
||||||
let mut padded_bytes = [0; 4];
|
|
||||||
padded_bytes[1..4].copy_from_slice(&bytes[offset..end]);
|
|
||||||
u32::from_be_bytes(padded_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get an unsigned 32 bit integer from a byte slice
|
|
||||||
fn read_u32(bytes: &[u8], offset: usize) -> u32 {
|
|
||||||
let size = (u32::BITS / 8) as usize;
|
|
||||||
let end: usize = offset + size;
|
|
||||||
u32::from_be_bytes(
|
|
||||||
bytes[offset..end]
|
|
||||||
.try_into()
|
|
||||||
.unwrap_or_else(|_| unreachable!("u32::BITS / 8 is always 4")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a copy of the `size` bytes in `file` at `offset`
|
|
||||||
pub(crate) fn read_from_file(file: &mut std::fs::File, offset: u64, size: usize) -> Result<Vec<u8>> {
|
|
||||||
file.seek(std::io::SeekFrom::Start(offset))?;
|
|
||||||
let mut bytes = vec![0; size];
|
|
||||||
file.read_exact(&mut bytes)?;
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces a hashmap containing offsets and sizes of .hps files contained within the iso
|
|
||||||
/// These can be looked up by TrackId
|
|
||||||
pub(crate) fn create_track_map(iso: &mut std::fs::File) -> Result<HashMap<TrackId, (u32, u32)>> {
|
|
||||||
const FST_LOCATION_OFFSET: u64 = 0x424;
|
|
||||||
const FST_SIZE_OFFSET: u64 = 0x0428;
|
|
||||||
const FST_ENTRY_SIZE: usize = 0xC;
|
|
||||||
|
|
||||||
// Filesystem Table (FST)
|
|
||||||
let fst_location = u32::from_be_bytes(
|
|
||||||
read_from_file(iso, FST_LOCATION_OFFSET, 0x4)?
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| anyhow!("Unable to read FST offset as u32"))?,
|
|
||||||
);
|
|
||||||
if fst_location <= 0 {
|
|
||||||
return Err(anyhow!("FST location is 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let fst_size = u32::from_be_bytes(
|
|
||||||
read_from_file(iso, FST_SIZE_OFFSET, 0x4)?
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| anyhow!("Unable to read FST size as u32"))?,
|
|
||||||
);
|
|
||||||
// Check for sensible size. Vanilla is 29993
|
|
||||||
if fst_size < 1000 || fst_size > 500000 {
|
|
||||||
return Err(anyhow!("FST size is out of range"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let fst = read_from_file(iso, fst_location as u64, fst_size as usize)?;
|
|
||||||
|
|
||||||
// FST String Table.
|
|
||||||
// TODO: This is the line that crashed on CISO. All above values are 0.
|
|
||||||
// I think a better fix than the defensive lines I added above could be to
|
|
||||||
// return Result<...> from read_u32 and handle errors? With the code as is,
|
|
||||||
// it's still technically possible maybe to get through here and then crash
|
|
||||||
// on the read_u32 calls in the filter_map
|
|
||||||
let str_table_offset = read_u32(&fst, 0x8) as usize * FST_ENTRY_SIZE;
|
|
||||||
|
|
||||||
// Collect the .hps file metadata in the FST into a hash map
|
|
||||||
Ok(fst[..str_table_offset]
|
|
||||||
.chunks(FST_ENTRY_SIZE)
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let is_file = entry[0] == 0;
|
|
||||||
let name_offset = str_table_offset + read_u24(entry, 0x1) as usize;
|
|
||||||
let offset = read_u32(entry, 0x4);
|
|
||||||
let size = read_u32(entry, 0x8);
|
|
||||||
|
|
||||||
let name = CStr::from_bytes_until_nul(&fst[name_offset..]).ok()?.to_str().ok()?;
|
|
||||||
|
|
||||||
if is_file && name.ends_with(".hps") {
|
|
||||||
match get_track_id_by_filename(&name) {
|
|
||||||
Some(track_id) => Some((track_id, (offset, size))),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a tuple containing a randomly selected menu track tournament track
|
|
||||||
/// to play
|
|
||||||
pub(crate) fn get_random_menu_tracks() -> (TrackId, TrackId) {
|
|
||||||
// 25% chance to use the alternate menu theme
|
|
||||||
let menu_track = if fastrand::u8(0..4) == 0 {
|
|
||||||
TrackId::Menu2
|
|
||||||
} else {
|
|
||||||
TrackId::Menu1
|
|
||||||
};
|
|
||||||
|
|
||||||
// 50% chance to use the alternate tournament mode theme
|
|
||||||
let tournament_track = if fastrand::u8(0..2) == 0 {
|
|
||||||
TrackId::TournamentMode1
|
|
||||||
} else {
|
|
||||||
TrackId::TournamentMode2
|
|
||||||
};
|
|
||||||
|
|
||||||
(menu_track, tournament_track)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the user is in an actual match
|
|
||||||
/// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua#L1177
|
|
||||||
pub(crate) fn is_in_game(scene_major: u8, scene_minor: u8) -> bool {
|
|
||||||
if scene_major == SCENE_ALL_STAR_MODE && scene_minor < SCENE_ALL_STAR_CSS {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_VS_MODE || scene_major == SCENE_VS_ONLINE {
|
|
||||||
return scene_minor == SCENE_VS_INGAME;
|
|
||||||
}
|
|
||||||
if (SCENE_TRAINING_MODE..=SCENE_STAMINA_MODE).contains(&scene_major) || scene_major == SCENE_FIXED_CAMERA_MODE {
|
|
||||||
return scene_minor == SCENE_TRAINING_INGAME;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_EVENT_MATCH {
|
|
||||||
return scene_minor == SCENE_EVENT_MATCH_INGAME;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_CLASSIC_MODE && scene_minor < SCENE_CLASSIC_CONTINUE {
|
|
||||||
return scene_minor % 2 == 1;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_ADVENTURE_MODE {
|
|
||||||
return scene_minor == SCENE_ADVENTURE_MUSHROOM_KINGDOM
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_MUSHROOM_KINGDOM_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_TINY_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_MUSHROOM_KONGO_JUNGLE_GIANT_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_UNDERGROUND_MAZE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_HYRULE_TEMPLE_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_BRINSTAR
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_ESCAPE_ZEBES
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_GREEN_GREENS_KIRBY_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_GREEN_GREENS_KIRBY_TEAM_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_GREEN_GREENS_GIANT_KIRBY_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_1
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_2
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_CORNERIA_BATTLE_3
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_POKEMON_STADIUM_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_FZERO_GRAND_PRIX_RACE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_FZERO_GRAND_PRIX_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_ONETT_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_ICICLE_MOUNTAIN_CLIMB
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_BATTLEFIELD_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_BATTLEFIELD_METAL_BATTLE
|
|
||||||
|| scene_minor == SCENE_ADVENTURE_FINAL_DESTINATION_BATTLE;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_TARGET_TEST {
|
|
||||||
return scene_minor == SCENE_TARGET_TEST_INGAME;
|
|
||||||
}
|
|
||||||
if (SCENE_SUPER_SUDDEN_DEATH..=MENU_LIGHTNING_MELEE).contains(&scene_major) {
|
|
||||||
return scene_minor == SCENE_SSD_INGAME;
|
|
||||||
}
|
|
||||||
if (SCENE_HOME_RUN_CONTEST..=SCENE_CRUEL_MELEE).contains(&scene_major) {
|
|
||||||
return scene_minor == SCENE_HOME_RUN_CONTEST_INGAME;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_TITLE_SCREEN_IDLE {
|
|
||||||
return scene_minor == SCENE_TITLE_SCREEN_IDLE_FIGHT_1 || scene_minor == SCENE_TITLE_SCREEN_IDLE_FIGHT_2;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the player navigating the menus (including CSS and SSS)
|
|
||||||
/// Sourced from M'Overlay: https://github.com/bkacjios/m-overlay/blob/d8c629d/source/melee.lua#L1243
|
|
||||||
pub(crate) fn is_in_menus(scene_major: u8, scene_minor: u8) -> bool {
|
|
||||||
if scene_major == SCENE_MAIN_MENU {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_VS_MODE {
|
|
||||||
return scene_minor == SCENE_VS_CSS || scene_minor == SCENE_VS_SSS;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_VS_ONLINE {
|
|
||||||
return scene_minor == SCENE_VS_ONLINE_CSS || scene_minor == SCENE_VS_ONLINE_SSS || scene_minor == SCENE_VS_ONLINE_RANKED;
|
|
||||||
}
|
|
||||||
if (SCENE_TRAINING_MODE..=SCENE_STAMINA_MODE).contains(&scene_major) || scene_major == SCENE_FIXED_CAMERA_MODE {
|
|
||||||
return scene_minor == SCENE_TRAINING_CSS || scene_minor == SCENE_TRAINING_SSS;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_EVENT_MATCH {
|
|
||||||
return scene_minor == SCENE_EVENT_MATCH_SELECT;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_CLASSIC_MODE || scene_major == SCENE_ADVENTURE_MODE || scene_major == SCENE_ALL_STAR_MODE {
|
|
||||||
return scene_minor == SCENE_CLASSIC_CSS;
|
|
||||||
}
|
|
||||||
if scene_major == SCENE_TARGET_TEST {
|
|
||||||
return scene_minor == SCENE_TARGET_TEST_CSS;
|
|
||||||
}
|
|
||||||
if (SCENE_SUPER_SUDDEN_DEATH..=MENU_LIGHTNING_MELEE).contains(&scene_major) {
|
|
||||||
return scene_minor == SCENE_SSD_CSS || scene_minor == SCENE_SSD_SSS;
|
|
||||||
}
|
|
||||||
if (SCENE_HOME_RUN_CONTEST..=SCENE_CRUEL_MELEE).contains(&scene_major) {
|
|
||||||
return scene_minor == SCENE_HOME_RUN_CONTEST_CSS;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
[toolchain]
|
|
||||||
channel = "1.70.0"
|
|
||||||
components = ["rustfmt", "rustc-dev"]
|
|
||||||
profile = "minimal"
|
|
2
Externals/SlippiRustExtensions/rustfmt.toml
vendored
2
Externals/SlippiRustExtensions/rustfmt.toml
vendored
|
@ -1,2 +0,0 @@
|
||||||
max_width = 130
|
|
||||||
match_block_trailing_comma = true
|
|
Loading…
Add table
Add a link
Reference in a new issue