Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto splitters usability improvements #30

Merged
merged 5 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,084 changes: 893 additions & 191 deletions Cargo.lock

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,35 @@ crate-type = ["cdylib"]

[dependencies]
obs = { path = "obs" }
livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features = ["software-rendering", "font-loading"] }
livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features = [
"software-rendering",
"font-loading",
] }
log = { version = "0.4.6", features = ["serde"] }

# crates needed for the auto splitter management code to work as expected
hyperx = { version = "1.4.0", optional = true }
open = { version = "5.0.0", optional = true }
quick-xml = { version = "0.30.0", features = [
"serialize",
"overlapped-lists",
], optional = true }
reqwest = { version = "0.11.18", features = [
"blocking",
"rustls-tls-native-roots",
], default-features = false, optional = true }
serde = { version = "1.0.166", features = ["derive"], optional = true }

[features]
default = ["auto-splitting"]
auto-splitting = ["livesplit-core/auto-splitting"]
auto-splitting = [
"livesplit-core/auto-splitting",
"dep:hyperx",
"dep:open",
"dep:quick-xml",
"dep:reqwest",
"dep:serde",
]

[profile.max-opt]
inherits = "release"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ name, where you can set hotkeys for the various actions.

If you add multiple sources that each use the same splits, but different
layouts, they all share the same state. This allows for a lot more complex
layouts than what is traditionally possible where could for example show the
layouts than what is traditionally possible where you could for example show the
splits on a completely different part of your stream than the timer itself.
43 changes: 43 additions & 0 deletions obs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,46 @@ pub extern "C" fn obs_properties_add_button(
) -> *mut obs_property_t {
panic!()
}

#[no_mangle]
pub extern "C" fn obs_properties_add_text(
_props: *mut obs_properties_t,
_name: *const c_char,
_description: *const c_char,
_text_type: obs_text_type,
) -> *mut obs_property_t {
panic!()
}
#[no_mangle]
pub extern "C" fn obs_property_set_modified_callback2(
_prop: *mut obs_property_t,
_modified2_callback: obs_property_modified2_t,
_private: *mut c_void,
) {
panic!()
}
#[no_mangle]
pub extern "C" fn obs_property_set_description(
_prop: *mut obs_property_t,
_description: *const c_char,
) {
panic!()
}
#[no_mangle]
pub extern "C" fn obs_property_set_enabled(_prop: *mut obs_property_t, _enabled: bool) {
panic!()
}
#[no_mangle]
pub extern "C" fn obs_properties_get(
_props: *mut obs_properties_t,
_prop: *const c_char,
) -> *mut obs_property_t {
panic!()
}
#[no_mangle]
pub extern "C" fn obs_module_get_config_path(
_module: *mut obs_module_t,
_file: *const c_char,
) -> *const c_char {
panic!()
}
212 changes: 212 additions & 0 deletions src/auto_splitters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use hyperx::header::{DispositionParam, Header};
use log::warn;
use quick_xml::{de, DeError};
use reqwest::header::{HeaderMap, CONTENT_DISPOSITION};
use serde::Deserialize;
use std::{
fs,
path::{Path, PathBuf},
str,
};

const LIST_FILE_NAME: &str = "LiveSplit.AutoSplitters.xml";

pub struct ListManager {
client: reqwest::blocking::Client,
list: Result<List, (GetListFromGithubError, GetListFromFileError)>,
list_xml_string: Option<String>,
}

pub enum GetListFromGithubError {
NetError(reqwest::Error),
DeserializationError(DeError),
}

pub enum GetListFromFileError {
IoError(std::io::Error),
DeserializationError(DeError),
}

#[derive(Deserialize, Clone)]
pub struct List {
#[serde(rename = "AutoSplitter")]
pub auto_splitters: Vec<AutoSplitter>,
}

#[derive(Deserialize, Clone)]
pub struct AutoSplitter {
#[serde(rename = "Games")]
pub games: Games,
#[serde(rename = "URLs")]
pub urls: Urls,
#[serde(rename = "Type")]
pub module_type: String,
#[serde(rename = "ScriptType")]
pub script_type: Option<String>,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "Website")]
pub website: Option<String>,
}

#[derive(Deserialize, Clone)]
pub struct Games {
#[serde(rename = "Game")]
pub games: Vec<String>,
}

#[derive(Deserialize, Clone)]
pub struct Urls {
#[serde(rename = "URL")]
pub urls: Vec<String>,
}

impl ListManager {
pub fn new(folder: &Path) -> Self {
let client = reqwest::blocking::Client::new();

let result = Self::get_list(&client, folder);

let (list, list_xml_string) = match result {
Ok((string, list)) => (Ok(list), Some(string)),
Err(e) => (Err(e), None),
};

Self {
client,
list,
list_xml_string,
}
}

pub fn get_result(&self) -> Result<(), &(GetListFromGithubError, GetListFromFileError)> {
match &self.list {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}

pub fn save_list_to_disk(&self, folder: &Path) -> bool {
if let Some(xml_string) = &self.list_xml_string {
fs::write(folder.join(LIST_FILE_NAME), xml_string).is_ok()
} else {
false
}
}

pub fn get_website_for_game(&self, game_name: &str) -> Option<&str> {
self.get_for_game(game_name)?.website.as_deref()
}

pub fn get_for_game(&self, game_name: &str) -> Option<&AutoSplitter> {
self.list
.as_ref()
.ok()?
.auto_splitters
.iter()
.find(|x| x.games.games.iter().any(|g| g == game_name))
}

//todo
pub fn download_for_game(&self, game_name: &str, folder: &Path) -> Option<PathBuf> {
self.download(self.get_for_game(game_name)?, folder)
}

pub fn download(&self, auto_splitter: &AutoSplitter, folder: &Path) -> Option<PathBuf> {
let mut file_paths = Vec::new();

for url in &auto_splitter.urls.urls {
let Ok(response) = self.client.get(url).send() else {
continue;
};

let file_name = Self::get_requested_file_name(response.headers()).unwrap_or_else(|| {
warn!("Couldn't get name for auto splitter file: {url}, defaulting to 'Unknown.wasm'");
"Unknown.wasm".into()
});

if let Ok(bytes) = response.bytes() {
let file_path = folder.join(&*file_name);

match fs::write(&file_path, bytes) {
Ok(_) => {
file_paths.push(file_path);
}
Err(e) => {
warn!("Something went wrong when downloading and saving auto splitter file: {url}: {e}")
}
}
}
}

file_paths
.into_iter()
.find(|path| path.extension().is_some_and(|e| e == "wasm"))
}

pub fn is_using_auto_splitting_runtime(auto_splitter: &AutoSplitter) -> bool {
auto_splitter
.script_type
.as_ref()
.is_some_and(|t| t == "AutoSplittingRuntime")
}

fn get_list(
client: &reqwest::blocking::Client,
folder: &Path,
) -> Result<(String, List), (GetListFromGithubError, GetListFromFileError)> {
let from_github_error = match Self::get_list_from_github(client) {
Ok(auto_splitters) => return Ok(auto_splitters),
Err(e) => e,
};

let from_file_error = match Self::get_list_from_file(folder) {
Ok(auto_splitters) => return Ok(auto_splitters),
Err(e) => e,
};

Err((from_github_error, from_file_error))
}

fn get_list_from_github(
client: &reqwest::blocking::Client,
) -> Result<(String, List), GetListFromGithubError> {
let url = "https://raw.githubusercontent.com/LiveSplit/LiveSplit.AutoSplitters/master/LiveSplit.AutoSplitters.xml";

let body = client
.get(url)
.send()
.map_err(GetListFromGithubError::NetError)?
.text()
.map_err(GetListFromGithubError::NetError)?;

match de::from_str(&body) {
Ok(auto_splitters) => Ok((body, auto_splitters)),
Err(e) => Err(GetListFromGithubError::DeserializationError(e)),
}
}

fn get_list_from_file(folder: &Path) -> Result<(String, List), GetListFromFileError> {
let buffer = fs::read_to_string(folder.join(LIST_FILE_NAME))
.map_err(GetListFromFileError::IoError)?;

match de::from_str::<List>(&buffer) {
Ok(auto_splitters) => Ok((buffer, auto_splitters)),
Err(e) => Err(GetListFromFileError::DeserializationError(e)),
}
}

// TODO: Make this return Result and improve support
fn get_requested_file_name(header_map: &HeaderMap) -> Option<Box<str>> {
hyperx::header::ContentDisposition::parse_header(&header_map.get(CONTENT_DISPOSITION)?)
.ok()?
.parameters
.into_iter()
.find_map(|param| {
let DispositionParam::Filename(_, _, bytes) = param else {
return None;
};
Some(str::from_utf8(&bytes).ok()?.into())
})
}
}
25 changes: 25 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ extern "C" {
description: *const c_char,
) -> *mut obs_property_t;
pub fn obs_data_get_bool(data: *mut obs_data_t, name: *const c_char) -> bool;
#[cfg(feature = "auto-splitting")]
pub fn obs_properties_add_text(
props: *mut obs_properties_t,
name: *const c_char,
description: *const c_char,
text_type: obs_text_type,
) -> *mut obs_property_t;
pub fn obs_properties_add_int(
props: *mut obs_properties_t,
name: *const c_char,
Expand Down Expand Up @@ -91,4 +98,22 @@ extern "C" {
text: *const c_char,
callback: obs_property_clicked_t,
) -> *mut obs_property_t;
pub fn obs_property_set_modified_callback2(
prop: *mut obs_property_t,
modified2_callback: obs_property_modified2_t,
private: *mut c_void,
);
#[cfg(feature = "auto-splitting")]
pub fn obs_property_set_description(prop: *mut obs_property_t, description: *const c_char);
#[cfg(feature = "auto-splitting")]
pub fn obs_property_set_enabled(prop: *mut obs_property_t, enabled: bool);
#[cfg(feature = "auto-splitting")]
pub fn obs_properties_get(
props: *mut obs_properties_t,
prop: *const c_char,
) -> *mut obs_property_t;
pub fn obs_module_get_config_path(
module: *mut obs_module_t,
file: *const c_char,
) -> *const c_char;
}
23 changes: 23 additions & 0 deletions src/ffi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub struct gs_texture {
pub type obs_base_effect = u32;
pub const OBS_EFFECT_PREMULTIPLIED_ALPHA: obs_base_effect = 7;

pub type obs_text_type = u32;
pub const OBS_TEXT_DEFAULT: obs_text_type = 0;
pub const OBS_TEXT_PASSWORD: obs_text_type = 1;
pub const OBS_TEXT_MULTILINE: obs_text_type = 2;
pub const OBS_TEXT_INFO: obs_text_type = 3;

pub type obs_data_t = obs_data;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
Expand Down Expand Up @@ -101,6 +107,23 @@ pub type obs_property_clicked_t = Option<
) -> bool,
>;

pub type obs_property_modified_t = Option<
unsafe extern "C" fn(
props: *mut obs_properties_t,
property: *mut obs_property_t,
settings: *mut obs_data_t,
) -> bool,
>;

pub type obs_property_modified2_t = Option<
unsafe extern "C" fn(
private: *mut c_void,
props: *mut obs_properties_t,
property: *mut obs_property_t,
settings: *mut obs_data_t,
) -> bool,
>;

pub type obs_property_t = obs_property;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
Expand Down
Loading