Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ThatNintendoNerd committed Sep 26, 2023
0 parents commit 44f8474
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
29 changes: 29 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "stage_config"
version = "0.1.0"
authors = ["ThatNintendoNerd"]
edition = "2021"

[package.metadata.skyline]
titleid = "01006A800016E000"

[lib]
crate-type = ["cdylib"]

[dependencies]
skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" }
smash = { git = "https://github.com/blu-dev/smash-rs.git" }
smash_stage = { path = "../smash_stage", features = ["serde"] }
hash40 = "1.1.0"
arcropolis-api = { git = "https://github.com/Raytwo/arcropolis_api.git" }
once_cell = "1.18.0"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8.0"
walkdir = "2"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
lto = true
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# stage_config

A [Skyline](https://github.com/skyline-dev/skyline) plugin for Super Smash Bros. Ultimate that enables the use and modification of stage features that are otherwise hardcoded into the game.

The latest release can be found [here](https://github.com/ThatNintendoNerd/stage_config/releases/latest).

## Features

Through the use of a configuration file, a stage mod can take advantage of the following features:

- New dynamic ground collisions
- Flatten or unflatten battle objects
- Custom center of gravity or the removal thereof
- Use of `stage_additional_setting` values from spirit battles outside of Spirits
- Discard specialized stage programming

For more information about these features, please read the [wiki](https://github.com/ThatNintendoNerd/stage_config/wiki).

## Building

NOTE: This project cannot be compiled without the smash_stage library. Said library is unreleased due to its incomplete state, but its release is planned.

With an up-to-date version of the Rust toolchain installed and [cargo-skyline](https://github.com/jam1garner/cargo-skyline) 3.0.0 or newer, run the following command to compile the project in release mode:

```
cargo skyline build --release
```

The resulting build is found at `./target/aarch64-skyline-switch/release/libstage_config.nro`
95 changes: 95 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::collections::{HashMap, HashSet};
use std::fs;

use hash40::Hash40;
use once_cell::sync::Lazy;
use serde::Deserialize;
use smash_stage::app::StageID;
use walkdir::WalkDir;

use crate::hooks::gravity::GravityParam;

pub static CONFIG: Lazy<Config> = Lazy::new(|| {
let mut config = Config::new();

for entry in WalkDir::new("sd:/ultimate/mods/")
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
{
let mut entry_path = entry.into_path();

if !arcropolis_api::is_mod_enabled(arcropolis_api::hash40(
entry_path.to_str().unwrap_or_default(),
)) {
continue;
}

entry_path.push("config_stage.toml");

if entry_path.is_file() {
if let Ok(string) = fs::read_to_string(&entry_path) {
match toml::from_str(&string) {
Ok(cfg) => config.merge(cfg),
Err(e) => {
eprintln!(
"[stage_config::config] Failed to parse TOML data from file '{}': {}",
entry_path.display(),
e
);
}
}
}
}
}

config
});

#[derive(Deserialize)]
pub struct Config {
#[serde(default)]
pub new_dynamic_collisions: HashMap<StageID, HashSet<Hash40>>,

#[serde(default)]
pub is_flat_stage: HashMap<StageID, bool>,

#[serde(default)]
pub gravity_param: HashMap<StageID, GravityParam>,

#[serde(default)]
pub stage_additional_settings: HashMap<StageID, i8>,

#[serde(default)]
pub discard_stage_code: Vec<StageID>,
}

impl Config {
fn new() -> Self {
Self {
new_dynamic_collisions: HashMap::new(),
is_flat_stage: HashMap::new(),
gravity_param: HashMap::new(),
stage_additional_settings: HashMap::new(),
discard_stage_code: Vec::new(),
}
}

fn merge(&mut self, other: Self) {
let Self {
new_dynamic_collisions,
is_flat_stage,
gravity_param,
stage_additional_settings,
discard_stage_code,
} = other;

self.new_dynamic_collisions.extend(new_dynamic_collisions);
self.is_flat_stage.extend(is_flat_stage);
self.gravity_param.extend(gravity_param);
self.stage_additional_settings
.extend(stage_additional_settings);
self.discard_stage_code.extend(discard_stage_code);
}
}
4 changes: 4 additions & 0 deletions src/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub(super) mod gravity;
pub(super) mod ground;
pub(super) mod settings;
pub(super) mod stage;
48 changes: 48 additions & 0 deletions src/hooks/gravity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use serde::Deserialize;
use smash::app;
use smash_stage::app::StageID;

use crate::config::CONFIG;

/// Parameters for gravity.
#[derive(Deserialize)]
pub struct GravityParam {
/// Boolean flag determining if the stage assumes a flat gravitational plane.
#[serde(default)]
is_gravity_normal: bool,

/// Center position of gravity.
#[serde(default)]
pos: Option<GravityCenter>,
}

/// Center position of gravity.
#[derive(Deserialize)]
struct GravityCenter {
/// Position along the x-axis.
#[serde(default)]
x: f32,

/// Position along the y-axis.
#[serde(default)]
y: f32,
}

pub fn set_gravity_param(stage_id: StageID) {
for (stage, param) in CONFIG.gravity_param.iter() {
if *stage == stage_id {
if let Some(instance) = app::BattleObjectWorld::instance_mut() {
if instance.is_gravity_normal != param.is_gravity_normal {
instance.is_gravity_normal = param.is_gravity_normal;
}

if !instance.is_gravity_normal {
if let Some(pos) = &param.pos {
instance.gravity_pos.x = pos.x;
instance.gravity_pos.y = pos.y;
}
}
}
}
}
}
27 changes: 27 additions & 0 deletions src/hooks/ground.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use smash_stage::app::StageBase;

use crate::config::CONFIG;

pub fn register_dynamic_collisions(stage_base: &StageBase) {
let stage_id = stage_base.stage_description.stage_id();

for (stage, models) in CONFIG.new_dynamic_collisions.iter() {
if stage_id == *stage {
for model_name in models.iter() {
unsafe {
for dynamic_object in
(*(*stage_base.level_data).dynamic_object_collection).iter()
{
if (**dynamic_object).name_hash == *model_name
&& stage_base.search_draw_model(*model_name).is_some()
{
stage_base.create_model_related_move_floor(&**dynamic_object);
}
}
}
}

break;
}
}
}
39 changes: 39 additions & 0 deletions src/hooks/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use smash_stage::app::{MeleeMode, SpiritsBattleData, StageDescription};

use crate::{config::CONFIG, offsets::OFFSETS};

#[skyline::hook(offset = OFFSETS.set_stage_random_settings)]
pub fn set_stage_random_settings(stage_description: &mut StageDescription, seed: u32) {
if !matches!(
MeleeMode::instance(),
MeleeMode::Standard
| MeleeMode::StandardMulti
| MeleeMode::SpiritsBattle
| MeleeMode::SpiritsBattleMulti
) {
let stage_id = stage_description.stage_id();

for (stage, setting) in CONFIG.stage_additional_settings.iter() {
if *stage == stage_id && *setting != 0 {
let mut spirits_battle_data = SpiritsBattleData::default();

spirits_battle_data.stage_id = stage_id;
spirits_battle_data.stage_additional_setting = *setting;

unsafe {
set_stage_additional_settings(&spirits_battle_data, stage_description);
}

break;
}
}
}

original!()(stage_description, seed);
}

#[skyline::from_offset(OFFSETS.set_stage_additional_settings)]
fn set_stage_additional_settings(
spirits_battle_data: &SpiritsBattleData,
stage_description: &mut StageDescription,
);
35 changes: 35 additions & 0 deletions src/hooks/stage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::mem;

use skyline::patching::Patch;
use smash_stage::app::{StageBase, StageID};

use crate::{config::CONFIG, hooks, offsets::OFFSETS};

#[skyline::hook(offset = OFFSETS.stage_base_pre_setup)]
pub fn stage_base_pre_setup(stage_base: &StageBase) {
original!()(stage_base);

hooks::ground::register_dynamic_collisions(stage_base);
hooks::gravity::set_gravity_param(stage_base.stage_description.stage_id());
}

#[skyline::hook(offset = OFFSETS.is_flat_stage)]
pub fn is_flat_stage(stage_id: StageID) -> bool {
for (stage, value) in CONFIG.is_flat_stage.iter() {
if *stage == stage_id {
return *value;
}
}

original!()(stage_id)
}

pub fn patch_create_stage_jump_table() {
for stage in CONFIG.discard_stage_code.iter() {
Patch::in_text(
OFFSETS.create_stage_jump_table + (*stage as usize) * mem::size_of::<StageID>(),
)
.data(0xFE12E38C_u32)
.unwrap();
}
}
20 changes: 20 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use once_cell::sync::Lazy;

mod config;
mod hooks;
mod offsets;

use config::CONFIG;
use offsets::OFFSETS;

#[skyline::main(name = "stage_config")]
fn main() {
Lazy::force(&OFFSETS);
skyline::install_hooks!(
hooks::stage::stage_base_pre_setup,
hooks::stage::is_flat_stage,
hooks::settings::set_stage_random_settings,
);
Lazy::force(&CONFIG);
hooks::stage::patch_create_stage_jump_table();
}
Loading

0 comments on commit 44f8474

Please sign in to comment.