feat: complete Steam execution
This commit is contained in:
@@ -8,8 +8,15 @@ use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use kiln_core::adapter::ProviderAdapter;
|
||||
use kiln_core::config::{
|
||||
ConfigurationLayerV1, ConfigurationSource, ConfigurationTier, built_in_configuration,
|
||||
configuration_path_for_current_user, load_configuration, resolve_layers,
|
||||
};
|
||||
use kiln_core::contract_error::{AmbiguousGameCandidate, ContractErrorCode, ContractErrorV1};
|
||||
use kiln_core::launch::LaunchPlanV1;
|
||||
use kiln_core::launch::{
|
||||
CommandArgument, CommandSpec, ControllerState, EnvironmentName, EnvironmentValue, Executable,
|
||||
LaunchPlanInput, LaunchPlanV1, LifecycleConfidence,
|
||||
};
|
||||
use kiln_core::{
|
||||
Capability, ContractName, ContractText, GameId, GameRecordInput, GameRecordV1, InstallPath,
|
||||
InstallationId,
|
||||
@@ -19,10 +26,11 @@ use uuid::Uuid;
|
||||
const MAX_VDF_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_LIBRARIES: usize = 128;
|
||||
const MAX_MANIFESTS: usize = 8192;
|
||||
const CAPABILITIES: &[Capability] = &[Capability::Discover, Capability::Info];
|
||||
const CAPABILITIES: &[Capability] = &[Capability::Discover, Capability::Info, Capability::Launch];
|
||||
const RECORD_CAPABILITIES: &[Capability] = &[
|
||||
Capability::Discover,
|
||||
Capability::Info,
|
||||
Capability::Launch,
|
||||
Capability::ClientRequired,
|
||||
];
|
||||
const INSTALLATION_NAMESPACE: u128 = 0x72d8_017f_93a9_47b7_0000_0000_0000_0000;
|
||||
@@ -32,6 +40,13 @@ const INSTALLATION_NAMESPACE: u128 = 0x72d8_017f_93a9_47b7_0000_0000_0000_0000;
|
||||
pub struct SteamAdapter {
|
||||
name: ContractName,
|
||||
root: PathBuf,
|
||||
client: SteamClient,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum SteamClient {
|
||||
Native,
|
||||
Flatpak,
|
||||
}
|
||||
|
||||
impl SteamAdapter {
|
||||
@@ -41,6 +56,7 @@ impl SteamAdapter {
|
||||
Self {
|
||||
name: name("steam"),
|
||||
root: root.into(),
|
||||
client: SteamClient::Native,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +72,15 @@ impl SteamAdapter {
|
||||
.ok_or_else(|| unavailable("no absolute home directory is available"))?;
|
||||
let native = home.join(".local/share/Steam");
|
||||
let flatpak = home.join(".var/app/com.valvesoftware.Steam/data/Steam");
|
||||
let root = if native.is_dir() { native } else { flatpak };
|
||||
Ok(Self::at(root))
|
||||
if native.is_dir() {
|
||||
Ok(Self::at(native))
|
||||
} else {
|
||||
Ok(Self {
|
||||
name: name("steam"),
|
||||
root: flatpak,
|
||||
client: SteamClient::Flatpak,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns matching records in deterministic App ID order.
|
||||
@@ -223,14 +246,110 @@ impl ProviderAdapter for SteamAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_launch(&self, _game_id: &GameId) -> Result<LaunchPlanV1, ContractErrorV1> {
|
||||
Err(ContractErrorV1::unsupported_capability(
|
||||
text("Steam launch planning begins in Phase 4"),
|
||||
name("launch"),
|
||||
))
|
||||
fn plan_launch(&self, game_id: &GameId) -> Result<LaunchPlanV1, ContractErrorV1> {
|
||||
let record = self
|
||||
.records()?
|
||||
.into_iter()
|
||||
.find(|record| record.id() == game_id)
|
||||
.ok_or_else(|| not_found("Steam game was not found"))?;
|
||||
let configuration_path = configuration_path_for_current_user()
|
||||
.map_err(|error| invalid(format!("resolve configuration path: {error}")))?;
|
||||
let mut layers = vec![built_in_configuration()];
|
||||
if let Some(user) = load_configuration(
|
||||
&configuration_path,
|
||||
ConfigurationSource {
|
||||
tier: ConfigurationTier::GlobalUser,
|
||||
label: text(&configuration_path.to_string_lossy()),
|
||||
},
|
||||
)
|
||||
.map_err(|error| invalid(format!("invalid user configuration: {error}")))?
|
||||
{
|
||||
layers.push(user);
|
||||
}
|
||||
layers.push(
|
||||
ConfigurationLayerV1::parse_toml(
|
||||
ConfigurationSource {
|
||||
tier: ConfigurationTier::Provider,
|
||||
label: text("Steam-managed runtime"),
|
||||
},
|
||||
"schema_version = 1\n[launch]\nruntime = \"steam-managed\"\n",
|
||||
)
|
||||
.expect("static Steam configuration"),
|
||||
);
|
||||
let resolved = resolve_layers(&layers)
|
||||
.map_err(|error| invalid(format!("resolve Steam configuration: {error}")))?;
|
||||
let mut arguments = match self.client {
|
||||
SteamClient::Native => Vec::new(),
|
||||
SteamClient::Flatpak => vec![argument("run"), argument("com.valvesoftware.Steam")],
|
||||
};
|
||||
arguments.extend([
|
||||
argument("-silent"),
|
||||
argument("-applaunch"),
|
||||
argument(game_id.value()),
|
||||
]);
|
||||
let executable = match self.client {
|
||||
SteamClient::Native => "steam",
|
||||
SteamClient::Flatpak => "flatpak",
|
||||
};
|
||||
let mut environment = minimal_environment();
|
||||
environment.extend(
|
||||
resolved
|
||||
.environment
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.value.clone())),
|
||||
);
|
||||
LaunchPlanV1::new(LaunchPlanInput {
|
||||
game_id: record.id().clone(),
|
||||
installation_id: record.installation_id(),
|
||||
store: name("steam"),
|
||||
adapter: name("steam"),
|
||||
configuration_owner: name("steam"),
|
||||
runtime: resolved.runtime.value,
|
||||
session_backend: resolved.session_backend.value,
|
||||
display_mode: resolved.display_mode.map(|mode| mode.value),
|
||||
controller_state: ControllerState::Unknown,
|
||||
working_directory: record.install_path().clone(),
|
||||
environment,
|
||||
wrappers: resolved
|
||||
.wrappers
|
||||
.into_iter()
|
||||
.map(|wrapper| wrapper.value)
|
||||
.collect(),
|
||||
provenance: resolved.provenance,
|
||||
lifecycle_confidence: LifecycleConfidence::Unknown,
|
||||
command: CommandSpec {
|
||||
executable: Executable::Program(name(executable)),
|
||||
arguments,
|
||||
},
|
||||
})
|
||||
.map_err(|_| invalid("Steam launch plan violated its contract"))
|
||||
}
|
||||
}
|
||||
|
||||
fn argument(raw: &str) -> CommandArgument {
|
||||
raw.to_owned().try_into().expect("validated Steam argument")
|
||||
}
|
||||
|
||||
fn minimal_environment() -> BTreeMap<EnvironmentName, EnvironmentValue> {
|
||||
[
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"DISPLAY",
|
||||
"HOME",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"PATH",
|
||||
"WAYLAND_DISPLAY",
|
||||
"XAUTHORITY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|key| {
|
||||
let value = env::var(key).ok()?;
|
||||
Some((key.parse().ok()?, value.try_into().ok()?))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_manifest(path: &Path, steamapps: &Path) -> Result<Option<GameRecordV1>, ContractErrorV1> {
|
||||
let input = read_text(path, "app manifest")?;
|
||||
let document = parse_vdf(&input)?;
|
||||
@@ -518,9 +637,10 @@ mod tests {
|
||||
use kiln_core::Capability;
|
||||
use kiln_core::adapter::ProviderAdapter;
|
||||
use kiln_core::contract_error::ContractErrorCode;
|
||||
use kiln_core::launch::{CommandArgument, Executable};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::SteamAdapter;
|
||||
use super::{SteamAdapter, SteamClient};
|
||||
|
||||
struct TestDirectory(PathBuf);
|
||||
|
||||
@@ -597,14 +717,50 @@ mod tests {
|
||||
[
|
||||
Capability::Discover,
|
||||
Capability::Info,
|
||||
Capability::Launch,
|
||||
Capability::ClientRequired
|
||||
]
|
||||
);
|
||||
assert_eq!(adapter.search("portal").unwrap().len(), 1);
|
||||
assert_eq!(adapter.resolve("Portal 2").unwrap().id(), record.id());
|
||||
let plan = adapter.plan_launch(record.id()).unwrap();
|
||||
assert_eq!(
|
||||
adapter.plan_launch(record.id()).unwrap_err().code(),
|
||||
ContractErrorCode::UnsupportedCapability
|
||||
plan.lifecycle_confidence(),
|
||||
kiln_core::launch::LifecycleConfidence::Unknown
|
||||
);
|
||||
assert!(matches!(
|
||||
&plan.command().executable,
|
||||
Executable::Program(program) if program.as_str() == "steam"
|
||||
));
|
||||
assert_eq!(
|
||||
plan.command()
|
||||
.arguments
|
||||
.iter()
|
||||
.map(CommandArgument::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
["-silent", "-applaunch", "620"]
|
||||
);
|
||||
|
||||
let mut flatpak = adapter.clone();
|
||||
flatpak.client = SteamClient::Flatpak;
|
||||
let plan = flatpak.plan_launch(record.id()).unwrap();
|
||||
assert!(matches!(
|
||||
&plan.command().executable,
|
||||
Executable::Program(program) if program.as_str() == "flatpak"
|
||||
));
|
||||
assert_eq!(
|
||||
plan.command()
|
||||
.arguments
|
||||
.iter()
|
||||
.map(CommandArgument::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
"run",
|
||||
"com.valvesoftware.Steam",
|
||||
"-silent",
|
||||
"-applaunch",
|
||||
"620"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user