mod enumeration;
mod envelope;
mod error_id;
pub use crate::data::enumeration::EnumString;
pub use crate::data::envelope::{
OpaqueValue, RequestEnvelope, RequestId, ResponseData, ResponseEnvelope, API_NAME, API_VERSION,
};
pub use crate::data::error_id::ErrorId;
use crate::data::enumeration::Enum;
use paste::paste;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
use std::convert::TryFrom;
pub trait Request: Serialize {
const MESSAGE_TYPE: EnumString<RequestType>;
type Response: Response;
}
pub trait Response: DeserializeOwned + Send + 'static {
const MESSAGE_TYPE: EnumString<ResponseType>;
}
pub trait EventData: Response {
type Config: EventConfig;
}
pub trait EventConfig: Serialize {
type Event: EventData;
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HotkeyAction {
Unset,
TriggerAnimation,
ChangeIdleAnimation,
ToggleExpression,
RemoveAllExpressions,
MoveModel,
ChangeBackground,
ReloadMicrophone,
ReloadTextures,
CalibrateCam,
#[serde(rename = "ChangeVTSModel")]
ChangeVtsModel,
TakeScreenshot,
ScreenColorOverlay,
RemoveAllItems,
ToggleItemScene,
DownloadRandomWorkshopItem,
ExecuteItemAction,
ArtMeshColorPreset,
ToggleTracker,
ToggleTwitchFeature,
LoadEffectPreset,
}
impl Default for HotkeyAction {
fn default() -> Self {
Self::Unset
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AnimationEventType {
Start,
End,
Custom,
}
impl Default for AnimationEventType {
fn default() -> Self {
Self::Custom
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ItemEventType {
Added,
Removed,
DroppedPinned,
DroppedUnpinned,
Clicked,
Locked,
Unlocked,
}
impl Default for ItemEventType {
fn default() -> Self {
Self::Clicked
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AngleRelativeTo {
RelativeToWorld,
RelativeToCurrentItemRotation,
RelativeToModel,
RelativeToPinPosition,
}
impl Default for AngleRelativeTo {
fn default() -> Self {
Self::RelativeToWorld
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SizeRelativeTo {
RelativeToWorld,
RelativeToCurrentItemSize,
}
impl Default for SizeRelativeTo {
fn default() -> Self {
Self::RelativeToWorld
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum VertexPinType {
Provided,
Center,
Random,
}
impl Default for VertexPinType {
fn default() -> Self {
Self::Provided
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Permission {
LoadCustomImagesAsItems,
}
impl Default for Permission {
fn default() -> Self {
Self::LoadCustomImagesAsItems
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PermissionStatus {
pub name: EnumString<Permission>,
pub granted: bool,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MouseButtonId(pub i32);
impl MouseButtonId {
pub fn is_left(&self) -> bool {
self.0 == 0
}
pub fn is_right(&self) -> bool {
self.0 == 1
}
pub fn is_middle(&self) -> bool {
self.0 == 2
}
}
macro_rules! define_request_response {
(
req_resp = [
$({
rust_name = $rust_name:ident,
$(req_name = $req_name:literal,)?
$(resp_name = $resp_name:literal,)?
$(#[doc = $req_doc:expr])+
$(#[derive($extra_derives:tt)])?
req = { $($req:tt)* },
$(#[doc = $resp_doc:expr])+
resp = $(( $($resp_inner:tt)+ ))? $({ $($resp_fields:tt)* })?,
},)*
],
events = [
$({
rust_name = $rust_event_name:ident,
$(event_name = $event_name:literal,)?
$(#[doc = $event_config_doc:expr])*
config = { $($event_config_fields:tt)* },
$(#[doc = $event_data_doc:expr])+
data = { $($event_data_fields:tt)* },
},)*
],
) => {
paste! {
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RequestType {
$(
$(#[serde(rename = $req_name)])?
[<$rust_name Request>],
)*
}
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ResponseType {
#[serde(rename = "APIError")]
ApiError,
$(
$(#[serde(rename = $resp_name)])?
[<$rust_name Response>],
)*
#[serde(rename = "VTubeStudioAPIStateBroadcast")]
VTubeStudioApiStateBroadcast,
$(
$(#[serde(rename = $event_name)])?
[<$rust_event_name Event>],
)*
}
impl ResponseType {
pub fn is_event(&self) -> bool {
match self {
$( Self::[<$rust_event_name Event>] => true, )*
_ => false
}
}
}
}
$(
paste! {
$(#[doc = $event_data_doc])+
#[doc = concat!("This event can be configured using [`", stringify!($rust_event_name), "EventConfig`].")]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct [<$rust_event_name Event>] { $($event_data_fields)* }
impl Response for [<$rust_event_name Event>] {
#[doc = concat!("[`ResponseType::", stringify!($rust_event_name), "Event`]")]
const MESSAGE_TYPE: EnumString<ResponseType> =
EnumString::new(ResponseType::[<$rust_event_name Event>]);
}
impl EventData for [<$rust_event_name Event>] {
#[doc = concat!("[`", stringify!($rust_event_name), "EventConfig`]")]
type Config = [<$rust_event_name EventConfig>];
}
}
)*
paste! {
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum Event {
$( $rust_event_name( [<$rust_event_name Event>] ), )*
Unknown(ResponseData),
}
impl TryFrom<ResponseData> for Event {
type Error = serde_json::Error;
fn try_from(data: ResponseData) -> Result<Self, Self::Error> {
Ok(match data.message_type.0 {
$(
Enum::Known(ResponseType::[<$rust_event_name Event>]) =>
Event::$rust_event_name(
data.data.deserialize::<[<$rust_event_name Event>]>()?
),
)*
_ => Event::Unknown(data),
})
}
}
$(
#[doc = concat!("Config for [`", stringify!($rust_event_name), "Event`].")]
$(#[doc = $event_config_doc])*
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct [<$rust_event_name EventConfig>] { $($event_config_fields)* }
impl EventConfig for [<$rust_event_name EventConfig>] {
type Event = [<$rust_event_name Event>];
}
)*
}
$(
paste! {
$(#[doc = $req_doc])+
#[doc = concat!("This request returns [`", stringify!($rust_name), "Response`].")]
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
$(#[derive($extra_derives)])*
#[serde(rename_all = "camelCase")]
pub struct [<$rust_name Request>] { $($req)* }
impl Request for [<$rust_name Request>] {
type Response = [<$rust_name Response>];
#[doc = concat!("[`RequestType::", stringify!($rust_name), "Request`]")]
const MESSAGE_TYPE: EnumString<RequestType> = EnumString::new(RequestType::[<$rust_name Request>]);
}
$(#[doc = $resp_doc])+
#[doc = concat!("This is the return value of [`", stringify!($rust_name), "Request`].")]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct [<$rust_name Response>] $(( $($resp_inner)+ );)? $({ $($resp_fields)* })?
impl Response for [<$rust_name Response>] {
#[doc = concat!("[`ResponseType::", stringify!($rust_name), "Response`]")]
const MESSAGE_TYPE: EnumString<ResponseType> = EnumString::new(ResponseType::[<$rust_name Response>]);
}
}
)*
};
}
impl EventSubscriptionRequest {
pub fn subscribe<T>(config: &T) -> Result<Self, serde_json::Error>
where
T: EventConfig,
{
Ok(Self {
subscribe: true,
event_name: Some(T::Event::MESSAGE_TYPE),
config: Some(OpaqueValue::new(config)?),
})
}
pub fn unsubscribe<T>() -> Self
where
T: EventData,
{
Self {
subscribe: false,
event_name: Some(T::MESSAGE_TYPE),
config: None,
}
}
pub fn unsubscribe_all() -> Self {
Self {
subscribe: false,
event_name: None,
config: None,
}
}
}
impl Default for RequestType {
fn default() -> Self {
Self::ApiStateRequest
}
}
impl Default for ResponseType {
fn default() -> Self {
Self::ApiStateResponse
}
}
define_request_response!(
req_resp = [{
rust_name = ApiState,
req_name = "APIStateRequest",
resp_name = "APIStateResponse",
#[derive(PartialEq)]
req = {},
resp = {
pub active: bool,
#[serde(rename = "vTubeStudioVersion")]
pub vtubestudio_version: String,
pub current_session_authenticated: bool,
},
},
{
rust_name = EventSubscription,
req = {
pub subscribe: bool,
pub event_name: Option<EnumString<ResponseType>>,
pub config: Option<OpaqueValue>,
},
resp = {
pub subscribed_event_count: i32,
pub subscribed_events: Vec<EnumString<ResponseType>>,
},
},
{
rust_name = AuthenticationToken,
#[derive(PartialEq)]
req = {
pub plugin_name: Cow<'static, str>,
pub plugin_developer: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin_icon: Option<Cow<'static, str>>,
},
resp = {
pub authentication_token: String,
},
},
{
rust_name = Authentication,
#[derive(PartialEq)]
req = {
pub plugin_name: Cow<'static, str>,
pub plugin_developer: Cow<'static, str>,
pub authentication_token: String,
},
resp = {
pub authenticated: bool,
pub reason: String,
},
},
{
rust_name = Statistics,
#[derive(PartialEq)]
req = {},
resp = {
pub uptime: i64,
pub framerate: i32,
#[serde(rename = "vTubeStudioVersion")]
pub vtubestudio_version: String,
pub allowed_plugins: i32,
pub connected_plugins: i32,
pub started_with_steam: bool,
pub window_width: i32,
pub window_height: i32,
pub window_is_fullscreen: bool,
},
},
{
rust_name = VtsFolderInfo,
req_name = "VTSFolderInfoRequest",
resp_name = "VTSFolderInfoResponse",
#[derive(PartialEq)]
req = {},
resp = {
pub models: String,
pub backgrounds: String,
pub items: String,
pub config: String,
pub logs: String,
pub backup: String,
},
},
{
rust_name = CurrentModel,
#[derive(PartialEq)]
req = {},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub vts_model_name: String,
pub vts_model_icon_name: String,
#[serde(rename = "live2DModelName")]
pub live2d_model_name: String,
pub model_load_time: i64,
pub time_since_model_loaded: i64,
#[serde(rename = "numberOfLive2DParameters")]
pub number_of_live2d_parameters: i32,
#[serde(rename = "numberOfLive2DArtmeshes")]
pub number_of_live2d_artmeshes: i32,
pub has_physics_file: bool,
pub number_of_textures: i32,
pub texture_resolution: i32,
pub model_position: ModelPosition,
},
},
{
rust_name = AvailableModels,
#[derive(PartialEq)]
req = {},
resp = {
pub number_of_models: i32,
pub available_models: Vec<Model>,
},
},
{
rust_name = ModelLoad,
#[derive(PartialEq)]
req = {
#[serde(rename = "modelID")]
pub model_id: String,
},
resp = {
#[serde(rename = "modelID")]
pub model_id: String,
},
},
{
rust_name = MoveModel,
#[derive(PartialEq)]
req = {
pub time_in_seconds: f64,
pub values_are_relative_to_model: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_x: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_y: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rotation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<f64>,
},
resp = {},
},
{
rust_name = HotkeysInCurrentModel,
#[derive(PartialEq)]
req = {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "modelID")]
pub model_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "live2DItemFileName")]
pub live2d_item_file_name: Option<String>,
},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub available_hotkeys: Vec<Hotkey>,
},
},
{
rust_name = HotkeyTrigger,
#[derive(PartialEq)]
req = {
#[serde(rename = "hotkeyID")]
pub hotkey_id: String,
#[serde(rename = "itemInstanceID", skip_serializing_if = "Option::is_none")]
pub item_instance_id: Option<String>,
},
resp = {
#[serde(rename = "hotkeyID")]
pub hotkey_id: String,
},
},
{
rust_name = ArtMeshList,
#[derive(PartialEq)]
req = {},
resp = {
pub model_loaded: bool,
pub number_of_art_mesh_names: i32,
pub number_of_art_mesh_tags: i32,
pub art_mesh_names: Vec<String>,
pub art_mesh_tags: Vec<String>,
},
},
{
rust_name = ColorTint,
#[derive(PartialEq)]
req = {
pub color_tint: ColorTint,
pub art_mesh_matcher: ArtMeshMatcher,
},
resp = {
pub matched_art_meshes: i32,
},
},
{
rust_name = SceneColorOverlayInfo,
#[derive(PartialEq)]
req = {},
resp = {
pub active: bool,
pub items_included: bool,
pub is_window_capture: bool,
pub base_brightness: i32,
pub color_boost: i32,
pub smoothing: i32,
pub color_overlay_r: i32,
pub color_overlay_g: i32,
pub color_overlay_b: i32,
pub color_avg_r: u8,
pub color_avg_g: u8,
pub color_avg_b: u8,
pub left_capture_part: CapturePart,
pub middle_capture_part: CapturePart,
pub right_capture_part: CapturePart,
},
},
{
rust_name = FaceFound,
#[derive(PartialEq)]
req = {},
resp = {
pub found: bool,
},
},
{
rust_name = InputParameterList,
#[derive(PartialEq)]
req = {},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub custom_parameters: Vec<Parameter>,
pub default_parameters: Vec<Parameter>,
},
},
{
rust_name = ParameterValue,
#[derive(PartialEq)]
req = {
pub name: String,
},
resp = (
pub Parameter
),
},
{
rust_name = Live2DParameterList,
#[derive(PartialEq)]
req = {},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub parameters: Vec<Parameter>,
},
},
{
rust_name = ParameterCreation,
#[derive(PartialEq)]
req = {
pub parameter_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
pub min: f64,
pub max: f64,
pub default_value: f64,
},
resp = {
pub parameter_name: String,
},
},
{
rust_name = ParameterDeletion,
#[derive(PartialEq)]
req = {
pub parameter_name: String,
},
resp = {
pub parameter_name: String,
},
},
{
rust_name = InjectParameterData,
#[derive(PartialEq)]
req = {
pub parameter_values: Vec<ParameterValue>,
pub face_found: bool,
pub mode: Option<EnumString<InjectParameterDataMode>>,
},
resp = {},
},
{
rust_name = ExpressionState,
#[derive(PartialEq)]
req = {
pub details: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub expression_file: Option<String>,
},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub expressions: Vec<Expression>,
},
},
{
rust_name = ExpressionActivation,
#[derive(PartialEq)]
req = {
pub expression_file: String,
pub active: bool,
},
resp = {},
},
{
rust_name = NdiConfig,
req_name = "NDIConfigRequest",
resp_name = "NDIConfigResponse",
#[derive(PartialEq)]
req = {
pub set_new_config: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub ndi_active: Option<bool>,
#[serde(rename = "useNDI5", skip_serializing_if = "Option::is_none")]
pub use_ndi5: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_custom_resolution: Option<bool>,
#[serde(rename = "customWidthNDI", serialize_with = "ndi_default_size")]
pub custom_width_ndi: Option<i32>,
#[serde(rename = "customHeightNDI", serialize_with = "ndi_default_size")]
pub custom_height_ndi: Option<i32>,
},
resp = {
pub set_new_config: bool,
pub ndi_active: bool,
#[serde(rename = "useNDI5")]
pub use_ndi5: bool,
pub use_custom_resolution: bool,
#[serde(rename = "customWidthNDI")]
pub custom_width_ndi: i32,
#[serde(rename = "customHeightNDI")]
pub custom_height_ndi: i32,
},
},
{
rust_name = GetCurrentModelPhysics,
#[derive(PartialEq)]
req = {},
resp = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub model_has_physics: bool,
pub physics_switched_on: bool,
pub using_legacy_physics: bool,
#[serde(rename = "physicsFPSSetting")]
pub physics_fps_setting: i32,
pub base_strength: i32,
pub base_wind: i32,
pub api_physics_override_active: bool,
pub api_physics_override_plugin_name: String,
pub physics_groups: Vec<PhysicsGroup>,
},
},
{
rust_name = SetCurrentModelPhysics,
#[derive(PartialEq)]
req = {
pub strength_overrides: Vec<PhysicsOverride>,
pub wind_overrides: Vec<PhysicsOverride>,
},
resp = {},
},
{
rust_name = ItemList,
#[derive(PartialEq)]
req = {
pub include_available_spots: bool,
pub include_item_instances_in_scene: bool,
pub include_available_item_files: bool,
pub only_items_with_file_name: Option<String>,
#[serde(rename = "onlyItemsWithInstanceID")]
pub only_items_with_instance_id: Option<String>,
},
resp = {
pub items_in_scene_count: i32,
pub total_items_allowed_count: i32,
pub can_load_items_right_now: bool,
pub available_spots: Vec<i32>,
pub item_instances_in_scene: Vec<ItemInstanceInScene>,
pub available_item_files: Vec<AvailableItemFile>,
},
},
{
rust_name = ItemLoad,
#[derive(PartialEq)]
req = {
pub file_name: String,
pub position_x: f64,
pub position_y: f64,
pub size: f64,
pub rotation: f64,
pub fade_time: f64,
pub order: i32,
pub fail_if_order_taken: bool,
pub smoothing: f64,
pub censored: bool,
pub flipped: bool,
pub locked: bool,
pub unload_when_plugin_disconnects: bool,
pub custom_data_base64: Option<String>,
pub custom_data_ask_user_first: bool,
pub custom_data_skip_asking_user_if_whitelisted: bool,
pub custom_data_ask_timer: f64,
},
resp = {
#[serde(rename = "instanceID")]
pub instance_id: String,
pub file_name: String,
},
},
{
rust_name = ItemUnload,
#[derive(PartialEq)]
req = {
pub unload_all_in_scene: bool,
pub unload_all_loaded_by_this_plugin: bool,
pub allow_unloading_items_loaded_by_user_or_other_plugins: bool,
#[serde(rename = "instanceIDs")]
pub instance_ids: Vec<String>,
pub file_names: Vec<String>,
},
resp = {
pub unloaded_items: Vec<UnloadedItem>,
},
},
{
rust_name = ItemAnimationControl,
#[derive(PartialEq)]
req = {
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub framerate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brightness: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub opacity: Option<f64>,
pub set_auto_stop_frames: bool,
pub auto_stop_frames: Vec<i32>,
pub set_animation_play_state: bool,
pub animation_play_state: bool,
},
resp = {
pub frame: i32,
pub animation_playing: bool,
},
},
{
rust_name = ItemMove,
#[derive(PartialEq)]
req = {
pub items_to_move: Vec<ItemToMove>,
},
resp = {
pub moved_items: Vec<MovedItem>,
},
},
{
rust_name = ArtMeshSelection,
#[derive(PartialEq)]
req = {
#[serde(skip_serializing_if = "Option::is_none")]
pub text_override: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help_override: Option<String>,
pub requested_art_mesh_count: i32,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub active_art_meshes: Vec<String>,
},
resp = {
pub success: bool,
pub active_art_meshes: Vec<String>,
pub inactive_art_meshes: Vec<String>,
},
},
{
rust_name = ItemPin,
#[derive(PartialEq)]
req = {
pub pin: bool,
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
pub angle_relative_to: EnumString<AngleRelativeTo>,
pub size_relative_to: EnumString<SizeRelativeTo>,
pub vertex_pin_type: EnumString<VertexPinType>,
pub pin_info: ArtMeshPosition,
},
resp = {
pub is_pinned: bool,
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
pub item_file_name: String,
},
},
{
rust_name = Permission,
#[derive(PartialEq)]
req = {
pub requested_permission: Option<EnumString<Permission>>,
},
resp = {
pub grant_success: bool,
pub requested_permission: Option<EnumString<Permission>>,
pub permissions: Vec<PermissionStatus>,
},
},
{
rust_name = PostProcessingList,
#[derive(PartialEq)]
req = {
pub fill_post_processing_presets_array: bool,
pub fill_post_processing_effects_array: bool,
#[serde(rename = "effectIDFilter", skip_serializing_if = "Vec::is_empty")]
pub effect_id_filter: Vec<String>,
},
resp = {
pub post_processing_supported: bool,
pub post_processing_active: bool,
pub can_send_post_processing_update_request_right_now: bool,
pub restricted_effects_allowed: bool,
pub preset_is_active: bool,
pub active_preset: String,
pub preset_count: i32,
pub active_effect_count: i32,
pub effect_count_before_filter: i32,
pub config_count_before_filter: i32,
pub effect_count_after_filter: i32,
pub config_count_after_filter: i32,
pub post_processing_effects: Vec<PostProcessingEffect>,
pub post_processing_presets: Vec<String>,
},
},
{
rust_name = PostProcessingUpdate,
#[derive(PartialEq)]
req = {
pub post_processing_on: bool,
pub set_post_processing_preset: bool,
pub set_post_processing_values: bool,
pub preset_to_set: String,
pub post_processing_fade_time: f64,
pub set_all_other_values_to_default: bool,
pub using_restricted_effects: bool,
pub randomize_all: bool,
pub randomize_all_chaos_level: f64,
pub post_processing_values: Vec<PostProcessingValue>,
},
resp = {
pub post_processing_active: bool,
pub preset_is_active: bool,
pub active_preset: String,
pub active_effect_count: i32,
},
}, ],
events = [
{
rust_name = Test,
config = {
pub test_message_for_event: String,
},
data = {
pub your_test_message: String,
pub counter: i32,
},
},
{
rust_name = ModelLoaded,
config = {
#[serde(rename = "modelID", skip_serializing_if = "Vec::is_empty")]
pub model_id: Vec<String>
},
data = {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
},
},
{
rust_name = TrackingStatusChanged,
config = {},
data = {
pub face_found: bool,
pub left_hand_found: bool,
pub right_hand_found: bool,
},
},
{
rust_name = BackgroundChanged,
config = {},
data = {
pub background_name: String,
},
},
{
rust_name = ModelConfigChanged,
config = {},
data = {
#[serde(rename = "modelID")]
pub model_id: String,
pub model_name: String,
pub hotkey_config_changed: bool,
},
},
{
rust_name = ModelMoved,
config = {},
data = {
#[serde(rename = "modelID")]
pub model_id: String,
pub model_name: String,
pub model_position: ModelPosition,
},
},
{
rust_name = ModelOutline,
config = {
pub draw: bool,
},
data = {
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub convex_hull: Vec<Vec2>,
pub convex_hull_center: Vec2,
pub window_size: Vec2,
},
},
{
rust_name = HotkeyTriggered,
config = {
pub only_for_action: Option<EnumString<HotkeyAction>>,
#[serde(rename = "ignoreHotkeysTriggeredByAPI")]
pub ignore_hotkeys_triggered_by_api: bool,
},
data = {
#[serde(rename = "hotkeyID")]
pub hotkey_id: String,
pub hotkey_name: String,
pub hotkey_action: EnumString<HotkeyAction>,
pub hotkey_file: String,
#[serde(rename = "hotkeyTriggeredByAPI")]
pub hotkey_triggered_by_api: bool,
#[serde(rename = "modelID")]
pub model_id: String,
pub model_name: String,
#[serde(rename = "isLive2DItem")]
pub is_live2d_item: bool,
},
},
{
rust_name = ModelAnimation,
config = {
#[serde(rename = "ignoreLive2DItems")]
pub ignore_live2d_items: bool,
pub ignore_idle_animations: bool,
},
data = {
pub animation_event_type: EnumString<AnimationEventType>,
pub animation_event_time: f64,
pub animation_event_data: String,
pub animation_name: String,
pub animation_length: f64,
pub is_idle_animation: bool,
#[serde(rename = "modelID")]
pub model_id: String,
pub model_name: String,
#[serde(rename = "isLive2DItem")]
pub is_live2d_item: bool,
},
},
{
rust_name = Item,
config = {
#[serde(rename = "itemInstanceIDs", skip_serializing_if = "Vec::is_empty")]
pub item_instance_ids: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub item_file_names: Vec<String>,
},
data = {
pub item_event_type: EnumString<ItemEventType>,
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
pub item_file_name: String,
pub item_position: Vec2,
},
},
{
rust_name = ModelClicked,
config = {
pub only_clicks_on_model: bool,
},
data = {
pub model_loaded: bool,
#[serde(rename = "loadedModelID")]
pub loaded_model_id: String,
pub loaded_model_name: String,
pub model_was_clicked: bool,
#[serde(rename = "mouseButtonID")]
pub mouse_button_id: MouseButtonId,
pub click_position: Vec2,
pub window_size: Vec2,
pub clicked_art_mesh_count: i32,
pub art_mesh_hits: Vec<ArtMeshHit>,
},
},
{
rust_name = PostProcessing,
config = {},
data = {
pub current_on_state: bool,
pub current_preset: String,
},
},
{
rust_name = Live2DCubismEditorConnected,
config = {},
data = {
pub trying_to_connect: bool,
pub connected: bool,
pub should_send_parameters: bool,
},
},
],
);
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtMeshHit {
pub art_mesh_order: i32,
pub is_masked: bool,
pub hit_info: ArtMeshPosition,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtMeshPosition {
#[serde(rename = "modelID")]
pub model_id: String,
#[serde(rename = "artMeshID")]
pub art_mesh_id: String,
pub angle: f64,
pub size: f64,
#[serde(rename = "vertexID1")]
pub vertex_id1: i32,
#[serde(rename = "vertexID2")]
pub vertex_id2: i32,
#[serde(rename = "vertexID3")]
pub vertex_id3: i32,
pub vertex_weight1: f64,
pub vertex_weight2: f64,
pub vertex_weight3: f64,
}
#[derive(Default, Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct Vec2 {
pub x: f64,
pub y: f64,
}
#[allow(missing_docs)]
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub enum InjectParameterDataMode {
#[serde(rename = "set")]
Set,
#[serde(rename = "add")]
Add,
}
impl Default for InjectParameterDataMode {
fn default() -> Self {
Self::Set
}
}
#[allow(missing_docs)]
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[non_exhaustive]
pub enum ItemType {
#[serde(rename = "PNG")]
Png,
#[serde(rename = "JPG")]
Jpg,
#[serde(rename = "GIF")]
Gif,
AnimationFolder,
#[serde(rename = "Live2D")]
Live2D,
Unknown,
}
impl Default for ItemType {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnloadedItem {
#[serde(rename = "instanceID")]
pub instance_id: String,
pub file_name: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemInstanceInScene {
pub file_name: String,
#[serde(rename = "instanceID")]
pub instance_id: String,
pub order: i32,
#[serde(rename = "type")]
pub type_: EnumString<ItemType>,
pub censored: bool,
pub flipped: bool,
pub locked: bool,
pub smoothing: f64,
pub framerate: f64,
pub frame_count: i32,
pub current_frame: i32,
pub pinned_to_model: bool,
#[serde(rename = "pinnedModelID")]
pub pinned_model_id: String,
#[serde(rename = "pinnedArtMeshID")]
pub pinned_art_mesh_id: String,
pub group_name: String,
pub scene_name: String,
pub from_workshop: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvailableItemFile {
pub file_name: String,
#[serde(rename = "type")]
pub type_: EnumString<ItemType>,
pub loaded_count: i32,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemToMove {
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
pub time_in_seconds: f64,
pub fade_mode: EnumString<FadeMode>,
#[serde(serialize_with = "item_move_default_i32")]
pub position_x: Option<i32>,
#[serde(serialize_with = "item_move_default_i32")]
pub position_y: Option<i32>,
#[serde(serialize_with = "item_move_default_f64")]
pub size: Option<f64>,
#[serde(serialize_with = "item_move_default_f64")]
pub rotation: Option<f64>,
#[serde(serialize_with = "item_move_default_i32")]
pub order: Option<i32>,
pub set_flip: bool,
pub flip: bool,
pub user_can_stop: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MovedItem {
#[serde(rename = "itemInstanceID")]
pub item_instance_id: String,
pub success: bool,
#[serde(
rename = "errorID",
serialize_with = "moved_item_error_serialize",
deserialize_with = "moved_item_error_deserialize"
)]
pub error_id: Option<ErrorId>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostProcessingEffect {
#[serde(rename = "internalID")]
pub internal_id: String,
#[serde(rename = "enumID")]
pub enum_id: String,
pub explanation: String,
pub effect_is_active: bool,
pub effect_is_restricted: bool,
pub config_entries: Vec<PostProcessingEffectConfigEntry>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostProcessingEffectConfigEntry {
#[serde(rename = "internalID")]
pub internal_id: String,
#[serde(rename = "enumID")]
pub enum_id: String,
pub explanation: String,
#[serde(rename = "type")]
pub type_: String,
pub activation_config: bool,
pub float_value: f64,
pub float_min: f64,
pub float_max: f64,
pub float_default: f64,
pub int_value: i32,
pub int_min: i32,
pub int_max: i32,
pub int_default: i32,
pub color_value: String,
pub color_default: String,
pub color_has_alpha: bool,
pub bool_value: bool,
pub bool_default: bool,
pub string_value: String,
pub string_default: String,
pub scene_item_value: String,
pub scene_item_default: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostProcessingValue {
#[serde(rename = "configID")]
pub config_id: String,
pub config_value: String,
}
fn moved_item_error_deserialize<'de, D>(deserializer: D) -> Result<Option<ErrorId>, D::Error>
where
D: Deserializer<'de>,
{
let id = i32::deserialize(deserializer)?;
if id == -1 {
Ok(None)
} else {
Ok(Some(ErrorId::new(id)))
}
}
fn moved_item_error_serialize<S>(value: &Option<ErrorId>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i32(match value {
Some(v) => v.as_i32(),
None => -1,
})
}
fn item_move_default_i32<S>(value: &Option<i32>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i32(value.unwrap_or(-1000))
}
fn item_move_default_f64<S>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_f64(value.unwrap_or(-1000.0f64))
}
#[allow(missing_docs)]
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum FadeMode {
Linear,
EaseIn,
EaseOut,
EaseBoth,
Overshoot,
Zip,
}
impl Default for FadeMode {
fn default() -> Self {
Self::Linear
}
}
fn ndi_default_size<S>(value: &Option<i32>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i32(value.unwrap_or(-1))
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[error("APIError {error_id}: {message}")]
pub struct ApiError {
#[serde(rename = "errorID")]
pub error_id: ErrorId,
pub message: String,
}
impl Response for ApiError {
const MESSAGE_TYPE: EnumString<ResponseType> = EnumString::new(ResponseType::ApiError);
}
impl ApiError {
pub fn is_unauthenticated(&self) -> bool {
self.error_id.is_unauthenticated()
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VTubeStudioApiStateBroadcast {
pub active: bool,
pub port: i32,
#[serde(rename = "instanceID")]
pub instance_id: String,
pub window_title: String,
}
impl Response for VTubeStudioApiStateBroadcast {
const MESSAGE_TYPE: EnumString<ResponseType> =
EnumString::new(ResponseType::VTubeStudioApiStateBroadcast);
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPosition {
pub position_x: f64,
pub position_y: f64,
pub rotation: f64,
pub size: f64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Model {
pub model_loaded: bool,
pub model_name: String,
#[serde(rename = "modelID")]
pub model_id: String,
pub vts_model_name: String,
pub vts_model_icon_name: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hotkey {
pub name: String,
#[serde(rename = "type")]
pub type_: EnumString<HotkeyAction>,
pub file: String,
#[serde(rename = "hotkeyID")]
pub hotkey_id: String,
pub description: Option<String>,
pub key_combination: Vec<String>,
#[serde(rename = "onScreenButtonID")]
pub on_screen_button_id: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorTint {
pub color_r: u8,
pub color_g: u8,
pub color_b: u8,
pub color_a: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub mix_with_scene_lighting_color: Option<f64>,
#[serde(rename = "jeb_")]
pub jeb_: bool,
}
impl Default for ColorTint {
fn default() -> Self {
Self {
color_r: 0,
color_g: 0,
color_b: 0,
color_a: 255,
mix_with_scene_lighting_color: None,
jeb_: false,
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtMeshMatcher {
pub tint_all: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub art_mesh_number: Vec<i32>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub name_exact: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub name_contains: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tag_exact: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tag_contains: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapturePart {
pub active: bool,
pub color_r: u8,
pub color_g: u8,
pub color_b: u8,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Parameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub added_by: Option<String>,
pub value: f64,
pub min: f64,
pub max: f64,
pub default_value: f64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParameterValue {
pub id: String,
pub value: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Expression {
pub name: String,
pub file: String,
pub active: bool,
pub deactivate_when_key_is_let_go: bool,
pub auto_deactivate_after_seconds: bool,
pub seconds_remaining: f64,
pub used_in_hotkeys: Vec<ExpressionUsedInHotkey>,
pub parameters: Vec<ExpressionParameter>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExpressionParameter {
pub name: String,
pub value: f64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExpressionUsedInHotkey {
pub name: String,
pub id: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PhysicsGroup {
#[serde(rename = "groupID")]
pub group_id: String,
pub group_name: String,
pub strength_multiplier: f64,
pub wind_multiplier: f64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PhysicsOverride {
pub id: String,
pub value: f64,
pub set_base_value: bool,
pub override_seconds: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn response_type_json() -> Result {
assert_eq!(
serde_json::from_value::<EnumString<ResponseType>>(json!("APIError"))?,
EnumString::new(ResponseType::ApiError),
);
assert_eq!(
serde_json::to_value::<EnumString<ResponseType>>(EnumString::new(
ResponseType::ApiError
))?,
json!("APIError"),
);
assert_eq!(
serde_json::from_value::<EnumString<ResponseType>>(json!("ColorTintResponse"))?,
ResponseType::ColorTintResponse,
);
assert_eq!(
serde_json::to_value::<EnumString<ResponseType>>(
ResponseType::ColorTintResponse.into()
)?,
json!("ColorTintResponse"),
);
assert_eq!(
serde_json::from_value::<EnumString<ResponseType>>(json!("WalfieResponse"))?,
EnumString::new_from_str("WalfieResponse"),
);
assert_eq!(
serde_json::to_value(EnumString::<ResponseType>::new_from_str("WalfieResponse"))?,
json!("WalfieResponse"),
);
Ok(())
}
#[test]
fn request() -> Result {
let mut req = RequestEnvelope::new(&ApiStateRequest {})?;
req.request_id = Some("MyIDWithLessThan64Characters".into());
let json = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"requestID": "MyIDWithLessThan64Characters",
"messageType": "APIStateRequest",
"data": {}
});
assert_eq!(serde_json::to_value(&req)?, json);
assert_eq!(serde_json::from_value::<RequestEnvelope>(json)?, req);
Ok(())
}
#[test]
fn response() -> Result {
let json = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"timestamp": 1625405710728i64,
"messageType": "APIStateResponse",
"requestID": "MyIDWithLessThan64Characters",
"data": {
"active": true,
"vTubeStudioVersion": "1.9.0",
"currentSessionAuthenticated": false
}
});
let resp = ResponseEnvelope {
api_name: "VTubeStudioPublicAPI".into(),
api_version: "1.0".into(),
request_id: "MyIDWithLessThan64Characters".into(),
timestamp: 1625405710728,
data: Ok(ResponseData {
message_type: ApiStateResponse::MESSAGE_TYPE.into(),
data: OpaqueValue::new(&ApiStateResponse {
active: true,
vtubestudio_version: "1.9.0".into(),
current_session_authenticated: false,
})?,
}),
};
assert_eq!(serde_json::to_value(&resp)?, json);
assert_eq!(serde_json::from_value::<ResponseEnvelope>(json)?, resp);
Ok(())
}
#[test]
fn api_error() -> Result {
let json = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"timestamp": 1625405710728i64,
"requestID": "SomeID",
"messageType": "APIError",
"data": {
"errorID": 1,
"message": "Error message"
}
});
let resp = ResponseEnvelope {
api_name: "VTubeStudioPublicAPI".into(),
api_version: "1.0".into(),
request_id: "SomeID".into(),
timestamp: 1625405710728,
data: Err(ApiError {
error_id: ErrorId::API_ACCESS_DEACTIVATED,
message: "Error message".into(),
}),
};
assert_eq!(serde_json::to_value(&resp)?, json);
assert_eq!(serde_json::from_value::<ResponseEnvelope>(json)?, resp);
Ok(())
}
#[test]
fn parameter_value_response() -> Result {
let json = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"timestamp": 1625405710728i64,
"requestID": "SomeID",
"messageType": "ParameterValueResponse",
"data": {
"name": "MyCustomParamName1",
"addedBy": "My Plugin Name",
"value": 12.4,
"min": -30.0,
"max": 30.0,
"defaultValue": 0.0
}
});
let resp = ResponseEnvelope {
api_name: "VTubeStudioPublicAPI".into(),
api_version: "1.0".into(),
request_id: "SomeID".into(),
timestamp: 1625405710728,
data: Ok(ResponseData {
message_type: ParameterValueResponse::MESSAGE_TYPE.into(),
data: OpaqueValue::new(&ParameterValueResponse(Parameter {
name: "MyCustomParamName1".into(),
added_by: Some("My Plugin Name".into()),
value: 12.4,
min: -30.0,
max: 30.0,
default_value: 0.0,
}))?,
}),
};
assert_eq!(serde_json::to_value(&resp)?, json);
assert_eq!(serde_json::from_value::<ResponseEnvelope>(json)?, resp);
Ok(())
}
#[test]
fn parse_response() -> Result {
let data = ApiStateResponse {
active: true,
vtubestudio_version: "1.9.0".into(),
current_session_authenticated: false,
};
let resp = ResponseEnvelope::new(&data)?;
let parsed = resp.parse::<ApiStateResponse>()?;
assert_eq!(parsed, data);
Ok(())
}
#[test]
fn serialize_event_request() -> Result {
let req = RequestEnvelope::new(&EventSubscriptionRequest::subscribe(&TestEventConfig {
test_message_for_event: "text the event will return".to_owned(),
})?)?
.with_id(Some("SomeID".into()));
let expected = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"requestID": "SomeID",
"messageType": "EventSubscriptionRequest",
"data": {
"eventName": "TestEvent",
"subscribe": true,
"config": {
"testMessageForEvent": "text the event will return"
}
}
});
assert_eq!(
serde_json::from_value::<RequestEnvelope>(expected.clone())?,
req
);
assert_eq!(serde_json::to_value(&req)?, expected);
Ok(())
}
#[test]
fn parse_response_as_event() -> Result {
let json = json!({
"apiName": "VTubeStudioPublicAPI",
"apiVersion": "1.0",
"timestamp": 1625405710728i64,
"requestID": "SomeID",
"messageType": "TestEvent",
"data": {
"yourTestMessage": "text the event will return",
"counter": 672
}
});
let resp = serde_json::from_value::<ResponseEnvelope>(json)?;
let parsed = resp.parse_event()?;
let expected = TestEvent {
your_test_message: "text the event will return".to_owned(),
counter: 672,
};
assert!(matches!(parsed, Event::Test(event) if event == expected));
Ok(())
}
}