diff --git a/core/src/avm2/globals/flash/system/capabilities.rs b/core/src/avm2/globals/flash/system/capabilities.rs index f5a989cd8a22..c92e244b5a71 100644 --- a/core/src/avm2/globals/flash/system/capabilities.rs +++ b/core/src/avm2/globals/flash/system/capabilities.rs @@ -32,21 +32,12 @@ pub fn get_version<'gc>( _this: Value<'gc>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { - let os = match activation.avm2().player_runtime { - PlayerRuntime::FlashPlayer => "WIN", - PlayerRuntime::AIR => { - if cfg!(windows) { - "WIN" - } else if cfg!(target_vendor = "apple") { - "MAC" - } else { - "LNX" - } - } - }; Ok(AvmString::new_utf8( activation.gc(), - format!("{os} {},0,0,0", activation.avm2().player_version), + activation + .context + .system + .get_version_string(activation.context.player_version), ) .into()) } diff --git a/core/src/player.rs b/core/src/player.rs index 66bffb5f371a..3252eb63c29d 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -2560,6 +2560,7 @@ pub struct PlayerBuilder { avm2_optimizer_enabled: bool, #[cfg(feature = "default_font")] default_font: bool, + custom_version_string: Option, } impl PlayerBuilder { @@ -2616,6 +2617,7 @@ impl PlayerBuilder { avm2_optimizer_enabled: true, #[cfg(feature = "default_font")] default_font: true, + custom_version_string: None, } } @@ -2843,6 +2845,13 @@ impl PlayerBuilder { self } + /// Override the version string reported by `Capabilities.version`/`$version`, to this custom one. + /// SWFs normally expect a string in the format of "1,2,3,4", and by default Ruffle reports `(player_version),0,0,0` (e.g. `32,0,0,0`) + pub fn with_custom_version_string(mut self, value: Option) -> Self { + self.custom_version_string = value; + self + } + fn create_gc_root<'gc>( gc_context: &'gc Mutation<'gc>, player_version: u8, @@ -2979,7 +2988,11 @@ impl PlayerBuilder { // TODO: AVM1 and AVM2 use separate RNGs (though algorithm is same), so this is technically incorrect. // See: https://github.com/ruffle-rs/ruffle/issues/20244 rng: AvmRng::default(), - system: SystemProperties::new(language), + system: SystemProperties::new( + self.player_runtime, + language, + self.custom_version_string, + ), page_url: self.page_url.clone(), transform_stack: TransformStack::new(), instance_counter: 0, diff --git a/core/src/system_properties.rs b/core/src/system_properties.rs index 5db14feed57d..a90f234aea4f 100644 --- a/core/src/system_properties.rs +++ b/core/src/system_properties.rs @@ -1,4 +1,5 @@ use crate::context::UpdateContext; +use crate::PlayerRuntime; use bitflags::bitflags; use core::fmt; use fluent_templates::{langid, LanguageIdentifier}; @@ -266,10 +267,29 @@ pub struct SystemProperties { pub cpu_architecture: CpuArchitecture, /// The highest supported h264 decoder level pub idc_level: String, + /// A custom version string to report, instead of player version + pub custom_version_string: Option, } impl SystemProperties { - pub fn new(language: LanguageIdentifier) -> Self { + pub fn new( + player_runtime: PlayerRuntime, + language: LanguageIdentifier, + custom_version_string: Option, + ) -> Self { + let (manufacturer, os) = match player_runtime { + PlayerRuntime::FlashPlayer => (Manufacturer::Windows, OperatingSystem::WindowsUnknown), + PlayerRuntime::AIR => { + if cfg!(windows) { + (Manufacturer::Windows, OperatingSystem::WindowsUnknown) + } else if cfg!(target_vendor = "apple") { + (Manufacturer::Macintosh, OperatingSystem::MacOs) + } else { + (Manufacturer::Linux, OperatingSystem::Linux) + } + } + }; + SystemProperties { //TODO: default to true on fp>=7, false <= 6 exact_settings: true, @@ -284,19 +304,28 @@ impl SystemProperties { pixel_aspect_ratio: 1_f32, // source: https://tracker.adobe.com/#/view/FP-3949775 dpi: 72_f32, - manufacturer: Manufacturer::Linux, - os: OperatingSystem::Linux, + manufacturer, + os, cpu_architecture: CpuArchitecture::X86, idc_level: "5.1".into(), + custom_version_string, } } pub fn get_version_string(&self, player_version: u8) -> String { - format!( - "{} {},0,0,0", - self.manufacturer.get_platform_name(), - player_version - ) + if let Some(custom_version_string) = &self.custom_version_string { + format!( + "{} {}", + self.manufacturer.get_platform_name(), + custom_version_string + ) + } else { + format!( + "{} {},0,0,0", + self.manufacturer.get_platform_name(), + player_version + ) + } } pub fn has_capability(&self, cap: SystemCapabilities) -> bool { diff --git a/desktop/assets/texts/en-US/settings.ftl b/desktop/assets/texts/en-US/settings.ftl index 06f455c84869..2d8ac7e5b3e7 100644 --- a/desktop/assets/texts/en-US/settings.ftl +++ b/desktop/assets/texts/en-US/settings.ftl @@ -84,6 +84,8 @@ scale-mode-force-tooltip = player-version = Player Version +custom-player-version-string = Custom Player Version + player-runtime = Player Runtime player-runtime-flash = Flash Player player-runtime-air = Adobe AIR diff --git a/desktop/src/cli.rs b/desktop/src/cli.rs index 517f1e1314cb..efab98bcf5ca 100644 --- a/desktop/src/cli.rs +++ b/desktop/src/cli.rs @@ -183,6 +183,11 @@ pub struct Opt { #[clap(long)] pub player_version: Option, + /// Override the version string reported by `Capabilities.version`/`$version`, to this custom one. + /// SWFs normally expect a string in the format of "1,2,3,4", and by default Ruffle reports `(player_version),0,0,0` (e.g. `32,0,0,0`) + #[clap(long)] + pub custom_player_version: Option, + /// The runtime to emulate (Flash Player or Adobe AIR) #[clap(long)] pub player_runtime: Option, diff --git a/desktop/src/gui/dialogs/open_dialog.rs b/desktop/src/gui/dialogs/open_dialog.rs index 4b331d6fb025..6b919d3e71ce 100644 --- a/desktop/src/gui/dialogs/open_dialog.rs +++ b/desktop/src/gui/dialogs/open_dialog.rs @@ -43,6 +43,7 @@ pub struct OpenDialog { load_behavior: OptionalField>, letterbox: OptionalField>, player_version: OptionalField>, + custom_player_version_string: OptionalField, player_runtime: OptionalField>, dummy_external_interface: OptionalField, upgrade_to_https: OptionalField, @@ -229,6 +230,10 @@ impl OpenDialog { defaults.player.player_version, NumberField::new(1..=NEWEST_PLAYER_VERSION, DEFAULT_PLAYER_VERSION), ); + let custom_player_version_string = OptionalField::new( + defaults.player.custom_player_version_string.clone(), + SingleLineTextField::new("32,0,0,0"), + ); let player_runtime = OptionalField::new( defaults.player.player_runtime, EnumDropdownField::new( @@ -280,6 +285,7 @@ impl OpenDialog { load_behavior, letterbox, player_version, + custom_player_version_string, player_runtime, dummy_external_interface, upgrade_to_https, @@ -499,6 +505,14 @@ impl OpenDialog { .ui(ui, &mut self.options.player.player_version, locale); ui.end_row(); + ui.label(text(locale, "custom-player-version-string")); + self.custom_player_version_string.ui( + ui, + &mut self.options.player.custom_player_version_string, + locale, + ); + ui.end_row(); + ui.label(text(locale, "player-runtime")); self.player_runtime .ui(ui, &mut self.options.player.player_runtime, locale); @@ -614,6 +628,40 @@ impl InnerField for UrlField { } } +struct SingleLineTextField { + hint: &'static str, +} + +impl SingleLineTextField { + pub fn new(hint: &'static str) -> Self { + Self { hint } + } +} + +impl InnerField for SingleLineTextField { + type Value = String; + type Result = String; + + fn value_if_missing(&self) -> Self::Value { + String::new() + } + + fn ui(&self, ui: &mut Ui, value: &mut Self::Value, error: bool, _locale: &LanguageIdentifier) { + TextEdit::singleline(value) + .hint_text(self.hint) + .text_color_opt(if error { + Some(ui.style().visuals.error_fg_color) + } else { + None + }) + .ui(ui); + } + + fn value_to_result(&self, value: &Self::Value) -> Result { + Ok(value.to_string()) + } +} + struct CookieField { hint: &'static str, } diff --git a/desktop/src/player.rs b/desktop/src/player.rs index 9330e807934a..da4bc5a451d1 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -84,6 +84,7 @@ impl From<&GlobalPreferences> for LaunchOptions { referer: value.cli.referer.clone(), cookie: value.cli.cookie.clone(), player_version: value.cli.player_version, + custom_player_version_string: value.cli.custom_player_version.clone(), player_runtime: value.cli.player_runtime, frame_rate: value.cli.frame_rate, dummy_external_interface: if value.cli.dummy_external_interface { diff --git a/frontend-utils/src/bundle/README.md b/frontend-utils/src/bundle/README.md index 23cdb9d46b78..c85454ee381e 100644 --- a/frontend-utils/src/bundle/README.md +++ b/frontend-utils/src/bundle/README.md @@ -1,34 +1,49 @@ # Ruffle Bundle (.ruf) format specification + A Ruffle Bundle is an easy way to package and share Flash games and any assets that are required to make the game work. A bundle can be a directory or a renamed zip file, and must contain at minimum a `ruffle-bundle.toml` file. + * [Ruffle Bundle (.ruf) format specification](#ruffle-bundle-ruf-format-specification) - * [Directory structure](#directory-structure) - * [`ruffle-bundle.toml` (Bundle information)](#ruffle-bundletoml-bundle-information) - * [`content/` (Flash content)](#content-flash-content) - * [`ruffle-bundle.toml` file specification](#ruffle-bundletoml-file-specification) - * [`[bundle]`](#bundle) - * [`name` - The name of the bundle](#name---the-name-of-the-bundle) - * [`url` - The url of the Flash content to open](#url---the-url-of-the-flash-content-to-open) - * [`[player]`](#player) - * [`[parameters]` - A list of parameters to pass to the starting movie](#parameters---a-list-of-parameters-to-pass-to-the-starting-movie) - * [`script_timeout` - Script execution timeout](#script_timeout---script-execution-timeout) - * [`base_url` - Base URL for relative file paths](#base_url---base-url-for-relative-file-paths) - * [`quality` - Quality that the content starts with](#quality---quality-that-the-content-starts-with) - * [`align` - Stage Alignment that the content starts with](#align---stage-alignment-that-the-content-starts-with) - * [`force_align` - Allow or disallow the content from changing its Stage Alignment](#force_align---allow-or-disallow-the-content-from-changing-its-stage-alignment) - * [`scale_mode` - Stage Scale Mode that the content starts with](#scale_mode---stage-scale-mode-that-the-content-starts-with) - * [`force_scale_mode` - Allow or disallow the content from changing its Stage Alignment](#force_scale_mode---allow-or-disallow-the-content-from-changing-its-stage-alignment) - * [`upgrade_http_to_https` - Whether to upgrade HTTP urls to HTTPS silently](#upgrade_http_to_https---whether-to-upgrade-http-urls-to-https-silently) - * [`load_behavior` - How Ruffle should load movies](#load_behavior---how-ruffle-should-load-movies) - * [`letterbox` - Controls visual letterboxing around the content](#letterbox---controls-visual-letterboxing-around-the-content) - * [`spoof_url` - URL to pretend the initial SWF is being loaded from](#spoof_url---url-to-pretend-the-initial-swf-is-being-loaded-from) - * [`version` - Version of the Flash Player to emulate](#version---version-of-the-flash-player-to-emulate) - * [`runtime` - Which type of runtime to emulate](#runtime---which-type-of-runtime-to-emulate) - * [`frame_rate` - Override the target frame rate of this movie](#frame_rate---override-the-target-frame-rate-of-this-movie) - * [`mock_external_interface` - Provide a mocked ExternalInterface](#mock_external_interface---provide-a-mocked-externalinterface) + * [Directory structure](#directory-structure) + * [`ruffle-bundle.toml` (Bundle information)](#ruffle-bundletoml-bundle-information) + * [`content/` (Flash content)](#content-flash-content) + * [`ruffle-bundle.toml` file specification](#ruffle-bundletoml-file-specification) + * [`[bundle]`](#bundle) + * [`name` - The name of the bundle](#name---the-name-of-the-bundle) + * [`url` - The url of the Flash content to open](#url---the-url-of-the-flash-content-to-open) + * [`[player]`](#player) + * [ + `[parameters]` - A list of parameters to pass to the starting movie](#parameters---a-list-of-parameters-to-pass-to-the-starting-movie) + * [`script_timeout` - Script execution timeout](#script_timeout---script-execution-timeout) + * [`base_url` - Base URL for relative file paths](#base_url---base-url-for-relative-file-paths) + * [`quality` - Quality that the content starts with](#quality---quality-that-the-content-starts-with) + * [ + `align` - Stage Alignment that the content starts with](#align---stage-alignment-that-the-content-starts-with) + * [ + `force_align` - Allow or disallow the content from changing its Stage Alignment](#force_align---allow-or-disallow-the-content-from-changing-its-stage-alignment) + * [ + `scale_mode` - Stage Scale Mode that the content starts with](#scale_mode---stage-scale-mode-that-the-content-starts-with) + * [ + `force_scale_mode` - Allow or disallow the content from changing its Stage Alignment](#force_scale_mode---allow-or-disallow-the-content-from-changing-its-stage-alignment) + * [ + `upgrade_http_to_https` - Whether to upgrade HTTP urls to HTTPS silently](#upgrade_http_to_https---whether-to-upgrade-http-urls-to-https-silently) + * [`load_behavior` - How Ruffle should load movies](#load_behavior---how-ruffle-should-load-movies) + * [ + `letterbox` - Controls visual letterboxing around the content](#letterbox---controls-visual-letterboxing-around-the-content) + * [ + `spoof_url` - URL to pretend the initial SWF is being loaded from](#spoof_url---url-to-pretend-the-initial-swf-is-being-loaded-from) + * [`version` - Version of the Flash Player to emulate](#version---version-of-the-flash-player-to-emulate) + * [ + `custom_version_string` - Custom version of the player to report](#custom_version_string---custom-version-of-the-player-to-report) + * [`runtime` - Which type of runtime to emulate](#runtime---which-type-of-runtime-to-emulate) + * [ + `frame_rate` - Override the target frame rate of this movie](#frame_rate---override-the-target-frame-rate-of-this-movie) + * [ + `mock_external_interface` - Provide a mocked ExternalInterface](#mock_external_interface---provide-a-mocked-externalinterface) + ## Directory structure @@ -39,20 +54,27 @@ A bundle can be a directory or a renamed zip file, and must contain at minimum a More files and folders may be added in the future, as this format is expanded upon. ### `ruffle-bundle.toml` (Bundle information) + This [toml](https://toml.io/) file is required and contains information that Ruffle needs to run this bundle. See [the ruffle-bundle.toml file specification](#ruffle-bundletoml-file-specification) for more details. ### `content/` (Flash content) -Every file (and subdirectory) within this directory will be accessible to the Flash content, exposed as a **virtual filesystem**. -To Flash content, this is accessible through `file:///` - for example, the file `/content/game.swf` is `file:///game.swf`. +Every file (and subdirectory) within this directory will be accessible to the Flash content, exposed as a **virtual +filesystem**. + +To Flash content, this is accessible through `file:///` - for example, the file `/content/game.swf` is +`file:///game.swf`. The file `/content/locale/en.xml` is `file:///locale/en.xml`. -You'll want to put the `.swf` file in here, along with any extra files it may need. Files outside this directory are **not** accessible to the content. +You'll want to put the `.swf` file in here, along with any extra files it may need. Files outside this directory are * +*not** accessible to the content. ## `ruffle-bundle.toml` file specification + The absolute minimum `ruffle-bundle.toml` looks like this: + ```toml [bundle] name = "Super Mario 63" @@ -65,32 +87,40 @@ The same is true if the toml document is malformed or corrupt. All other fields are absolutely optional and reasonable defaults will be assumed if they're missing or invalid. ### `[bundle]` + This section is required to exist, and contains the two required fields for a bundle to work in Ruffle: #### `name` - The name of the bundle + This can be anything, and is shown to the user in UI. Try to keep this a reasonable length, and descriptive about what this bundle actually *is*. #### `url` - The url of the Flash content to open -Whilst this **can** be a URL on the internet (for example, `url = "https://ruffle.rs/demo/logo-anim.swf"` is totally valid), + +Whilst this **can** be a URL on the internet (for example, `url = "https://ruffle.rs/demo/logo-anim.swf"` is totally +valid), we would recommend that it instead be the path to a file within the `content/` directory inside the bundle. This way, an internet connection is not required, and the bundle won't stop working in 5 years when the website changes. -Remember - the `content/` directory is accessible through `file:///` - so if you have a game at `content/game.swf`, you'll want to use `url = "file:///game.swf"`. - +Remember - the `content/` directory is accessible through `file:///` - so if you have a game at `content/game.swf`, +you'll want to use `url = "file:///game.swf"`. ### `[player]` + This section contains player options, which change how Ruffle emulates the content in this bundle. These options may be overridden by users. #### `[parameters]` - A list of parameters to pass to the starting movie -These are sometimes also called 'FlashVars' by Flash developers. This is appended to any parameters given in the query string of the `bundle.url`. + +These are sometimes also called 'FlashVars' by Flash developers. This is appended to any parameters given in the query +string of the `bundle.url`. All values must be strings. Example: + ```toml [player.parameters] key = "value" @@ -98,46 +128,58 @@ favourite_number = "5" ``` #### `script_timeout` - Script execution timeout -How long a single script execution (e.g. a frame of ActionScript 3) can take, before it's considered to be stuck or broken. + +How long a single script execution (e.g. a frame of ActionScript 3) can take, before it's considered to be stuck or +broken. Value should be in seconds - fractional values are allowed. Example with a 5-second limit on scripts: + ```toml [player] script_timeout = 5 ``` #### `base_url` - Base URL for relative file paths + By default, this is the `bundle.url`, but some content may require something else. -When content looks up a **relative** path, as opposed to an absolute path, it makes it relative to this base url instead. +When content looks up a **relative** path, as opposed to an absolute path, it makes it relative to this base url +instead. Example: + ```toml [player] base_url = "https://example.org" ``` #### `quality` - Quality that the content starts with + The movie has the capacity to change this automatically at runtime, and the user may also change it at will. -The default value generally depends on the users hardware, and it's advisable to leave it to do so unless content requires +The default value generally depends on the users hardware, and it's advisable to leave it to do so unless content +requires a specific quality for aesthetics. -Whilst Flash [does technically support many quality modes](https://web.archive.org/web/20240420201659/https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/StageQuality.html); +Whilst +Flash [does technically support many quality modes](https://web.archive.org/web/20240420201659/https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/StageQuality.html); Ruffle currently only implements `low`, `medium` and `high`. Example: + ```toml [player] quality = "low" ``` #### `align` - Stage Alignment that the content starts with + The movie has the capacity to change this automatically at runtime, unless `player.force_align` is set to `true`. This controls the position of the movie after scaling to fill the viewport. This may be one of the following values: + - `bottom`: Specifies that the Stage is aligned at the bottom. - `bottom_left`: Specifies that the Stage is aligned in the bottom-left corner. - `bottom_right`: Specifies that the Stage is aligned in the bottom-right corner. @@ -149,85 +191,118 @@ This may be one of the following values: - `center` (Default): Specified that the Stage is aligned in the center. Example: + ```toml [player] align = "bottom_right" ``` #### `force_align` - Allow or disallow the content from changing its Stage Alignment + If set to `true`, content may not change its own Stage Alignment value (see `player.align`). Default is `false`. #### `scale_mode` - Stage Scale Mode that the content starts with + The movie has the capacity to change this automatically at runtime, unless `player.force_scale_mode` is set to `true`. This controls the behavior when the player viewport size differs from the SWF size. This may be one of the following values: + - `exact_fit`: The movie will be stretched to fit the container. - `no_border`: The movie will maintain its aspect ratio, but will be cropped. - `no_scale`: The movie is not scaled to fit the container, and the content is assumed to adjust itself with scripts. -- `show_all` (Default): The movie will scale to fill the container and maintain its aspect ratio, but will be letterboxed. +- `show_all` (Default): The movie will scale to fill the container and maintain its aspect ratio, but will be + letterboxed. Example: + ```toml [player] scale_mode = "no_border" ``` #### `force_scale_mode` - Allow or disallow the content from changing its Stage Alignment + If set to `true`, content may not change its own Stage Scale Mode value (see `player.scale_mode`). Default is `false`. #### `upgrade_http_to_https` - Whether to upgrade HTTP urls to HTTPS silently + If `true`, all `http://` URLs will be replaced with `https://`. #### `load_behavior` - How Ruffle should load movies + Some movies expect to be streamed, or expect to load instantly. This allows you to work around any potential issues by forcing a specific loading behaviour. This may be one of the following values: + - `streaming` (Default): Allow movies to execute before they have finished loading. - `delayed`: Delay execution of loaded movies until they have finished loading. - `blocking`: Block Ruffle until movies have finished loading. #### `letterbox` - Controls visual letterboxing around the content + If the contents aspect ratio does not match the players aspect ratio, Ruffle may put up letterboxes for aesthetics and to hide objects that perhaps should not be visible. This may be one of the following values: + - `off`: The content will not be letterboxed, everything will be visible. - `fullscreen`: The content will only be letterboxed if viewed in Full Screen and the aspect ratio doesn't match. - `on` (Default): The content will always be letterboxed if the aspect ratio doesn't match. #### `spoof_url` - URL to pretend the initial SWF is being loaded from -If set to a valid URL, we will **pretend** that the `bundle.url` SWF is actually being loaded from this location instead. + +If set to a valid URL, we will **pretend** that the `bundle.url` SWF is actually being loaded from this location +instead. This is often required for site locks that check if the content is being loaded from the original domain. We do not *actually* load the URL, and all other assets/SWFs are not affected. #### `version` - Version of the Flash Player to emulate + Whilst it's not common, some content depends on behaviour from specific Flash Players. You may set this to force Ruffle to try and emulate that behaviour. Default is likely to be `32`, but may be subject to change. +#### `custom_version_string` - Custom version of the player to report + +This overrides the version string reported by `Capabilities.version`/`$version`, which by default will be something +similar to `32,0,0,0` (but depends on `version`). + +This does not change any player behavior, only changing the actual `version` will do that. + +Ordinarily SWFs expect to see four digits separated by a comma - e.g. `32,0,0,465`. +You are not required to stick to this format, but you may break content that expects it. + #### `runtime` - Which type of runtime to emulate -Unfortunately content does not indicate which runtime it wants, so if there's AIR content it must be called out by setting + +Unfortunately content does not indicate which runtime it wants, so if there's AIR content it must be called out by +setting this value appropriately. This may be one of the following values: + - `flash_player` (Default): The original Flash Player, no thrills. - `air`: The Adobe AIR runtime, an extension of Flash Player with more native capabilities. #### `frame_rate` - Override the target frame rate of this movie + If set, the movies preferred frame rate is ignored and this value is used instead. -May be used to speed up or slow down movies, but some content keeps its own time tracking and may not be affected. Value can both be an integer and a fractional value. +May be used to speed up or slow down movies, but some content keeps its own time tracking and may not be affected. Value +can both be an integer and a fractional value. Example: + ```toml [player] frame_rate = 30.0 ``` #### `mock_external_interface` - Provide a mocked ExternalInterface -Some content used JavaScript calls to query things like the page URL. By setting this value to `true`, Ruffle will provide + +Some content used JavaScript calls to query things like the page URL. By setting this value to `true`, Ruffle will +provide a mocked up ExternalInterface that responds to some of the common JavaScript calls appropriately. diff --git a/frontend-utils/src/parse.rs b/frontend-utils/src/parse.rs index 022a72d29284..05f5c95a3e5c 100644 --- a/frontend-utils/src/parse.rs +++ b/frontend-utils/src/parse.rs @@ -226,6 +226,18 @@ pub trait ReadExt<'a> { result } + fn get_string(&'a self, cx: &mut ParseContext, key: &'static str) -> Option { + let mut result = None; + + cx.push_key(key); + if let Some(str) = self.get_impl(key).and_then(|item| item.as_str_or_warn(cx)) { + result = Some(str.to_owned()) + } + cx.pop_key(); + + result + } + fn get_bool(&'a self, cx: &mut ParseContext, key: &'static str) -> Option { cx.push_key(key); let result = self.get_impl(key).and_then(|x| x.as_bool_or_warn(cx)); diff --git a/frontend-utils/src/player_options.rs b/frontend-utils/src/player_options.rs index ef50f1b70182..076681f8c767 100644 --- a/frontend-utils/src/player_options.rs +++ b/frontend-utils/src/player_options.rs @@ -27,6 +27,7 @@ pub struct PlayerOptions { pub referer: Option, pub cookie: Option, pub player_version: Option, + pub custom_player_version_string: Option, pub player_runtime: Option, pub frame_rate: Option, pub dummy_external_interface: Option, @@ -52,6 +53,10 @@ impl PlayerOptions { referer: self.referer.clone().or_else(|| other.referer.clone()), cookie: self.cookie.clone().or_else(|| other.cookie.clone()), player_version: self.player_version.or(other.player_version), + custom_player_version_string: self + .custom_player_version_string + .clone() + .or_else(|| other.custom_player_version_string.clone()), player_runtime: self.player_runtime.or(other.player_runtime), frame_rate: self.frame_rate.or(other.frame_rate), dummy_external_interface: self diff --git a/frontend-utils/src/player_options/read.rs b/frontend-utils/src/player_options/read.rs index 9313fa83ad5d..a1e93663c7b7 100644 --- a/frontend-utils/src/player_options/read.rs +++ b/frontend-utils/src/player_options/read.rs @@ -60,6 +60,9 @@ pub fn read_player_options<'a>( // Player version result.player_version = table.get_integer(cx, "version").map(|x| x as u8); + // Custom player version string + result.custom_player_version_string = table.get_string(cx, "custom_version_string"); + // Player runtime result.player_runtime = table.parse_from_str(cx, "runtime"); @@ -621,6 +624,16 @@ mod tests { result.values() ); assert_eq!(Vec::::new(), result.warnings); + + let result = read("custom_version_string = \"1,2,3,4\""); + assert_eq!( + &PlayerOptions { + custom_player_version_string: Some("1,2,3,4".into()), + ..Default::default() + }, + result.values() + ); + assert_eq!(Vec::::new(), result.warnings); } #[test] diff --git a/frontend-utils/src/player_options/write.rs b/frontend-utils/src/player_options/write.rs index 62cb66bda7c6..0b5e56efc7ee 100644 --- a/frontend-utils/src/player_options/write.rs +++ b/frontend-utils/src/player_options/write.rs @@ -179,6 +179,18 @@ impl<'a> PlayerOptionsWriter<'a> { }) } + pub fn set_custom_player_version_string(&mut self, version: Option) { + self.0.edit(|options, toml_document| { + if let Some(version) = &version { + toml_document["custom_version_string"] = value(version.to_owned()); + } else { + toml_document.remove("custom_version_string"); + } + + options.custom_player_version_string = version; + }) + } + pub fn set_player_runtime(&mut self, player_runtime: Option) { self.0.edit(|options, toml_document| { if let Some(player_runtime) = player_runtime { @@ -234,6 +246,7 @@ pub fn write_player_options(writer: &mut PlayerOptionsWriter, options: &PlayerOp writer.set_letterbox(options.letterbox); writer.set_spoof_url(options.spoof_url.clone()); writer.set_player_version(options.player_version); + writer.set_custom_player_version_string(options.custom_player_version_string.clone()); writer.set_player_runtime(options.player_runtime); writer.set_frame_rate(options.frame_rate); writer.set_dummy_external_interface(options.dummy_external_interface); @@ -599,6 +612,24 @@ mod tests { ); } + #[test] + fn custom_player_version() { + test( + "", + |writer| writer.set_custom_player_version_string(Some("1,2,3,4".to_string())), + "custom_version_string = \"1,2,3,4\"\n", + ); + } + + #[test] + fn custom_player_version_remove() { + test( + "custom_version_string = \"1,2,3,4\"\n", + |writer| writer.set_custom_player_version_string(None), + "", + ); + } + #[test] fn player_runtime_air() { test( diff --git a/tests/tests/swfs/avm1/geturl/output.txt b/tests/tests/swfs/avm1/geturl/output.txt index 5d57be0727d3..71b8d4f62239 100644 --- a/tests/tests/swfs/avm1/geturl/output.txt +++ b/tests/tests/swfs/avm1/geturl/output.txt @@ -4,4 +4,4 @@ Navigator::navigate_to_url: Method: POST Param: value2=2 Param: value1=string - Param: $version=LNX 32,0,0,0 + Param: $version=WIN 32,0,0,0 diff --git a/web/packages/core/src/internal/builder.ts b/web/packages/core/src/internal/builder.ts index 8fe7502beebd..e17a0b4f31ec 100644 --- a/web/packages/core/src/internal/builder.ts +++ b/web/packages/core/src/internal/builder.ts @@ -88,6 +88,9 @@ export function configureBuilder( if (isExplicit(config.playerVersion)) { builder.setPlayerVersion(config.playerVersion); } + if (isExplicit(config.customPlayerVersionString)) { + builder.setCustomPlayerVersionString(config.customPlayerVersionString); + } if (isExplicit(config.preferredRenderer)) { builder.setPreferredRenderer(config.preferredRenderer); } diff --git a/web/packages/core/src/public/config/default.ts b/web/packages/core/src/public/config/default.ts index c7d21e7ebebf..5d1d10f9aa35 100644 --- a/web/packages/core/src/public/config/default.ts +++ b/web/packages/core/src/public/config/default.ts @@ -44,6 +44,7 @@ export const DEFAULT_CONFIG: Required = { publicPath: null, polyfills: true, playerVersion: null, + customPlayerVersionString: null, preferredRenderer: null, openUrlMode: OpenURLMode.Allow, allowNetworking: NetworkingAccessMode.All, diff --git a/web/packages/core/src/public/config/load-options.ts b/web/packages/core/src/public/config/load-options.ts index 18523741ac89..1fe358f47dbc 100644 --- a/web/packages/core/src/public/config/load-options.ts +++ b/web/packages/core/src/public/config/load-options.ts @@ -602,6 +602,20 @@ export interface BaseLoadOptions { */ playerVersion?: number | null; + /** + * A custom version string to be reported through `Capabilities.version`/`$version`. + * + * By default, this will be something similar to `32,0,0,0` (depending on {@link playerVersion}). + * + * This does not change any player behavior, only changing the actual {@link playerVersion} will do that. + * + * Ordinarily SWFs expect to see four digits separated by a comma - e.g. `32,0,0,465`. + * You are not required to stick to this format, but you may break content that expects it. + * + * @default null + */ + customPlayerVersionString?: string | null; + /** * The preferred render backend of the Ruffle player. * diff --git a/web/src/builder.rs b/web/src/builder.rs index 998f46233ed5..71fa73f678f2 100644 --- a/web/src/builder.rs +++ b/web/src/builder.rs @@ -54,6 +54,7 @@ pub struct RuffleInstanceBuilder { pub(crate) log_level: tracing::Level, pub(crate) max_execution_duration: Duration, pub(crate) player_version: Option, + pub(crate) custom_player_version_string: Option, pub(crate) preferred_renderer: Option, // TODO: Enumify? pub(crate) open_url_mode: OpenUrlMode, pub(crate) allow_networking: NetworkingAccessMode, @@ -93,6 +94,7 @@ impl Default for RuffleInstanceBuilder { log_level: tracing::Level::ERROR, max_execution_duration: Duration::from_secs_f64(15.0), player_version: None, + custom_player_version_string: None, preferred_renderer: None, open_url_mode: OpenUrlMode::Allow, allow_networking: NetworkingAccessMode::All, @@ -247,6 +249,11 @@ impl RuffleInstanceBuilder { self.player_version = value; } + #[wasm_bindgen(js_name = "setCustomPlayerVersionString")] + pub fn set_custom_player_version_string(&mut self, value: Option) { + self.custom_player_version_string = value; + } + #[wasm_bindgen(js_name = "setPreferredRenderer")] pub fn set_preferred_renderer(&mut self, value: Option) { self.preferred_renderer = value; @@ -690,6 +697,7 @@ impl RuffleInstanceBuilder { .with_letterbox(self.letterbox) .with_max_execution_duration(self.max_execution_duration) .with_player_version(self.player_version) + .with_custom_version_string(self.custom_player_version_string.clone()) .with_player_runtime(self.player_runtime) .with_compatibility_rules(self.compatibility_rules.clone()) .with_quality(self.quality)