use core::ffi::{c_char, c_void};
use std::fmt;
use std::ptr;
use serde::{Deserialize, Serialize};
#[cfg(feature = "async")]
use doom_fish_utils::completion::{error_from_cstr, AsyncCompletion};
#[cfg(feature = "async")]
use doom_fish_utils::stream::{AsyncStreamSender, BoundedAsyncStream};
use crate::asset_pack::AssetPackSnapshot;
#[cfg(feature = "async")]
use crate::asset_pack::{collect_asset_packs, AssetPack, AssetPackStatus};
use crate::error::{BackgroundAssetsError, BridgeErrorPayload};
use crate::ffi;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DownloadProgress {
pub completed_unit_count: i64,
pub total_unit_count: i64,
pub fraction_completed: f64,
pub localized_description: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct UpdateCheck {
pub updating_ids: Vec<String>,
pub removed_ids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DownloadStatusUpdate {
Began {
asset_pack: AssetPackSnapshot,
},
Paused {
asset_pack: AssetPackSnapshot,
},
Downloading {
asset_pack: AssetPackSnapshot,
progress: DownloadProgress,
},
Finished {
asset_pack: AssetPackSnapshot,
},
Failed {
asset_pack: AssetPackSnapshot,
error: BackgroundAssetsError,
},
}
pub struct AssetPackManager {
ptr: *mut c_void,
}
impl AssetPackManager {
pub fn shared() -> Option<Self> {
Self::from_raw(unsafe { ffi::ba_asset_pack_manager_shared() })
}
fn from_raw(ptr: *mut c_void) -> Option<Self> {
(!ptr.is_null()).then_some(Self { ptr })
}
pub fn asset_pack_is_available_locally(&self, asset_pack_id: &str) -> bool {
ffi::required_cstring(asset_pack_id, "asset_pack_id").is_ok_and(|asset_pack_id| unsafe {
ffi::ba_asset_pack_manager_asset_pack_is_available_locally(
self.ptr,
asset_pack_id.as_ptr(),
)
})
}
pub fn contents(
&self,
path: &str,
asset_pack_id: Option<&str>,
) -> Result<Vec<u8>, BackgroundAssetsError> {
let path = ffi::required_cstring(path, "path")?;
let asset_pack_id_cstr = asset_pack_id
.map(|value| ffi::required_cstring(value, "asset_pack_id"))
.transpose()?;
let mut length = 0isize;
let mut error: *mut c_char = ptr::null_mut();
let bytes = unsafe {
ffi::ba_asset_pack_manager_contents(
self.ptr,
path.as_ptr(),
asset_pack_id_cstr
.as_ref()
.map_or(ptr::null(), |value| value.as_ptr()),
&mut length,
&mut error,
)
};
if !error.is_null() {
return Err(BackgroundAssetsError::from_owned_json_ptr(error));
}
Ok(unsafe { ffi::owned_bytes(bytes, length) })
}
pub fn descriptor(
&self,
path: &str,
asset_pack_id: Option<&str>,
) -> Result<i32, BackgroundAssetsError> {
let path = ffi::required_cstring(path, "path")?;
let asset_pack_id_cstr = asset_pack_id
.map(|value| ffi::required_cstring(value, "asset_pack_id"))
.transpose()?;
let mut error: *mut c_char = ptr::null_mut();
let descriptor = unsafe {
ffi::ba_asset_pack_manager_descriptor(
self.ptr,
path.as_ptr(),
asset_pack_id_cstr
.as_ref()
.map_or(ptr::null(), |value| value.as_ptr()),
&mut error,
)
};
if !error.is_null() {
return Err(BackgroundAssetsError::from_owned_json_ptr(error));
}
Ok(descriptor)
}
pub fn url(&self, path: &str) -> Result<String, BackgroundAssetsError> {
let path = ffi::required_cstring(path, "path")?;
let mut error: *mut c_char = ptr::null_mut();
let url = unsafe { ffi::ba_asset_pack_manager_url(self.ptr, path.as_ptr(), &mut error) };
if !error.is_null() {
return Err(BackgroundAssetsError::from_owned_json_ptr(error));
}
Ok(unsafe { ffi::owned_string(url) })
}
#[cfg(feature = "async")]
pub async fn all_asset_packs(&self) -> Result<Vec<AssetPack>, BackgroundAssetsError> {
let (future, ctx) = AsyncCompletion::<OpaquePtr>::create();
unsafe { ffi::ba_asset_pack_manager_all_asset_packs_async(self.ptr, ctx, object_async_cb) };
let OpaquePtr(ptr) = future.await.map_err(BackgroundAssetsError::message)?;
Ok(collect_asset_packs(ptr))
}
#[cfg(feature = "async")]
pub async fn asset_pack(
&self,
asset_pack_id: &str,
) -> Result<AssetPack, BackgroundAssetsError> {
let asset_pack_id = ffi::required_cstring(asset_pack_id, "asset_pack_id")?;
let (future, ctx) = AsyncCompletion::<OpaquePtr>::create();
unsafe {
ffi::ba_asset_pack_manager_asset_pack_async(
self.ptr,
asset_pack_id.as_ptr(),
ctx,
object_async_cb,
);
};
future
.await
.map_err(BackgroundAssetsError::message)
.and_then(|OpaquePtr(ptr)| {
AssetPack::from_raw(ptr).ok_or_else(|| {
BackgroundAssetsError::message("asset-pack pointer must not be null")
})
})
}
#[cfg(feature = "async")]
pub async fn status_relative_to(
&self,
asset_pack: &AssetPack,
) -> Result<AssetPackStatus, BackgroundAssetsError> {
let (future, ctx) = AsyncCompletion::<String>::create();
unsafe {
ffi::ba_asset_pack_manager_status_relative_async(
self.ptr,
asset_pack.raw_ptr(),
ctx,
string_async_cb,
);
};
let value = future.await.map_err(BackgroundAssetsError::message)?;
let bits = value.parse::<u64>().map_err(|error| {
BackgroundAssetsError::message(format!("invalid asset-pack status payload: {error}"))
})?;
Ok(AssetPackStatus::new(bits))
}
#[cfg(feature = "async")]
pub async fn local_status(
&self,
asset_pack_id: &str,
) -> Result<AssetPackStatus, BackgroundAssetsError> {
let asset_pack_id = ffi::required_cstring(asset_pack_id, "asset_pack_id")?;
let (future, ctx) = AsyncCompletion::<String>::create();
unsafe {
ffi::ba_asset_pack_manager_local_status_async(
self.ptr,
asset_pack_id.as_ptr(),
ctx,
string_async_cb,
);
};
let value = future.await.map_err(BackgroundAssetsError::message)?;
let bits = value.parse::<u64>().map_err(|error| {
BackgroundAssetsError::message(format!("invalid local-status payload: {error}"))
})?;
Ok(AssetPackStatus::new(bits))
}
#[cfg(feature = "async")]
pub async fn ensure_local_availability(
&self,
asset_pack: &AssetPack,
require_latest_version: bool,
) -> Result<(), BackgroundAssetsError> {
let (future, ctx) = AsyncCompletion::<String>::create();
unsafe {
ffi::ba_asset_pack_manager_ensure_local_availability_async(
self.ptr,
asset_pack.raw_ptr(),
require_latest_version,
ctx,
string_async_cb,
);
};
future.await.map_err(BackgroundAssetsError::message)?;
Ok(())
}
#[cfg(feature = "async")]
pub async fn check_for_updates(&self) -> Result<UpdateCheck, BackgroundAssetsError> {
let (future, ctx) = AsyncCompletion::<String>::create();
unsafe {
ffi::ba_asset_pack_manager_check_for_updates_async(self.ptr, ctx, string_async_cb);
};
let payload = future.await.map_err(BackgroundAssetsError::message)?;
let bridge: UpdateCheckPayload = serde_json::from_str(&payload).map_err(|error| {
BackgroundAssetsError::message(format!("invalid update-check payload: {error}"))
})?;
Ok(UpdateCheck {
updating_ids: bridge.updating_ids,
removed_ids: bridge.removed_ids,
})
}
#[cfg(feature = "async")]
pub async fn remove(&self, asset_pack_id: &str) -> Result<(), BackgroundAssetsError> {
let asset_pack_id = ffi::required_cstring(asset_pack_id, "asset_pack_id")?;
let (future, ctx) = AsyncCompletion::<String>::create();
unsafe {
ffi::ba_asset_pack_manager_remove_async(
self.ptr,
asset_pack_id.as_ptr(),
ctx,
string_async_cb,
);
};
future.await.map_err(BackgroundAssetsError::message)?;
Ok(())
}
#[cfg(feature = "async")]
pub fn status_updates(
&self,
capacity: usize,
) -> Result<DownloadStatusStream, BackgroundAssetsError> {
self.make_status_stream(None, capacity)
}
#[cfg(feature = "async")]
pub fn status_updates_for_asset_pack(
&self,
asset_pack_id: &str,
capacity: usize,
) -> Result<DownloadStatusStream, BackgroundAssetsError> {
self.make_status_stream(Some(asset_pack_id), capacity)
}
#[cfg(feature = "async")]
fn make_status_stream(
&self,
asset_pack_id: Option<&str>,
capacity: usize,
) -> Result<DownloadStatusStream, BackgroundAssetsError> {
let (stream, sender) = BoundedAsyncStream::new(capacity.max(1));
let ctx = Box::into_raw(Box::new(StreamContext { sender })).cast::<c_void>();
let asset_pack_id_cstr = asset_pack_id
.map(|value| ffi::required_cstring(value, "asset_pack_id"))
.transpose()?;
let bridge_ptr = unsafe {
ffi::ba_asset_pack_manager_status_updates_stream_create(
self.ptr,
asset_pack_id_cstr
.as_ref()
.map_or(ptr::null(), |value| value.as_ptr()),
ctx,
stream_callback,
)
};
if bridge_ptr.is_null() {
unsafe { drop(Box::from_raw(ctx.cast::<StreamContext>())) };
return Err(BackgroundAssetsError::message(
"failed to create asset-pack status stream",
));
}
Ok(DownloadStatusStream { stream, bridge_ptr })
}
}
impl Clone for AssetPackManager {
fn clone(&self) -> Self {
Self {
ptr: ffi::retained(self.ptr),
}
}
}
impl Drop for AssetPackManager {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::ba_object_release(self.ptr) };
}
}
}
impl fmt::Debug for AssetPackManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AssetPackManager").finish_non_exhaustive()
}
}
unsafe impl Send for AssetPackManager {}
unsafe impl Sync for AssetPackManager {}
#[cfg(feature = "async")]
pub struct DownloadStatusStream {
stream: BoundedAsyncStream<DownloadStatusUpdate>,
bridge_ptr: *mut c_void,
}
#[cfg(feature = "async")]
impl DownloadStatusStream {
pub fn next(&self) -> impl std::future::Future<Output = Option<DownloadStatusUpdate>> + '_ {
self.stream.next()
}
pub fn try_next(&self) -> Option<DownloadStatusUpdate> {
self.stream.try_next()
}
pub fn is_closed(&self) -> bool {
self.stream.is_closed()
}
pub fn buffered_count(&self) -> usize {
self.stream.buffered_count()
}
}
#[cfg(feature = "async")]
impl fmt::Debug for DownloadStatusStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DownloadStatusStream")
.field("buffered_count", &self.buffered_count())
.field("is_closed", &self.is_closed())
.finish()
}
}
#[cfg(feature = "async")]
impl Drop for DownloadStatusStream {
fn drop(&mut self) {
if !self.bridge_ptr.is_null() {
unsafe { ffi::ba_object_release(self.bridge_ptr) };
}
}
}
#[derive(Deserialize)]
struct AssetPackSnapshotPayload {
id: String,
#[serde(rename = "downloadSize")]
download_size: i64,
version: i64,
description: String,
}
#[derive(Deserialize)]
struct ProgressPayload {
#[serde(rename = "completedUnitCount")]
completed_unit_count: i64,
#[serde(rename = "totalUnitCount")]
total_unit_count: i64,
#[serde(rename = "fractionCompleted")]
fraction_completed: f64,
#[serde(rename = "localizedDescription")]
localized_description: String,
}
#[cfg(feature = "async")]
#[derive(Deserialize)]
struct UpdateCheckPayload {
#[serde(rename = "updatingIDs")]
updating_ids: Vec<String>,
#[serde(rename = "removedIDs")]
removed_ids: Vec<String>,
}
#[derive(Deserialize)]
struct DownloadStatusUpdatePayload {
kind: String,
#[serde(rename = "assetPack")]
asset_pack: AssetPackSnapshotPayload,
progress: Option<ProgressPayload>,
error: Option<BridgeErrorPayload>,
}
impl From<AssetPackSnapshotPayload> for AssetPackSnapshot {
fn from(value: AssetPackSnapshotPayload) -> Self {
Self {
id: value.id,
download_size: value.download_size,
version: value.version,
description: value.description,
}
}
}
impl From<ProgressPayload> for DownloadProgress {
fn from(value: ProgressPayload) -> Self {
Self {
completed_unit_count: value.completed_unit_count,
total_unit_count: value.total_unit_count,
fraction_completed: value.fraction_completed,
localized_description: value.localized_description,
}
}
}
impl TryFrom<DownloadStatusUpdatePayload> for DownloadStatusUpdate {
type Error = BackgroundAssetsError;
fn try_from(value: DownloadStatusUpdatePayload) -> Result<Self, Self::Error> {
let asset_pack = value.asset_pack.into();
Ok(match value.kind.as_str() {
"began" => Self::Began { asset_pack },
"paused" => Self::Paused { asset_pack },
"downloading" => Self::Downloading {
asset_pack,
progress: value
.progress
.ok_or_else(|| BackgroundAssetsError::message("missing progress payload"))?
.into(),
},
"finished" => Self::Finished { asset_pack },
"failed" => Self::Failed {
asset_pack,
error: value
.error
.map(Into::into)
.ok_or_else(|| BackgroundAssetsError::message("missing error payload"))?,
},
other => {
return Err(BackgroundAssetsError::message(format!(
"unknown download status update kind: {other}"
)))
}
})
}
}
#[cfg(feature = "async")]
struct OpaquePtr(*mut c_void);
#[cfg(feature = "async")]
unsafe impl Send for OpaquePtr {}
#[cfg(feature = "async")]
unsafe extern "C" fn object_async_cb(result: *mut c_void, error: *const c_char, ctx: *mut c_void) {
if !error.is_null() {
let message = unsafe { error_from_cstr(error) };
unsafe { AsyncCompletion::<OpaquePtr>::complete_err(ctx, message) };
} else if !result.is_null() {
unsafe { AsyncCompletion::complete_ok(ctx, OpaquePtr(result)) };
} else {
unsafe { AsyncCompletion::<OpaquePtr>::complete_err(ctx, "missing object result".into()) };
}
}
#[cfg(feature = "async")]
unsafe extern "C" fn string_async_cb(result: *mut c_void, error: *const c_char, ctx: *mut c_void) {
if !error.is_null() {
let message = unsafe { error_from_cstr(error) };
unsafe { AsyncCompletion::<String>::complete_err(ctx, message) };
} else if !result.is_null() {
let value = unsafe { ffi::owned_string(result.cast::<c_char>()) };
unsafe { AsyncCompletion::complete_ok(ctx, value) };
} else {
unsafe { AsyncCompletion::<String>::complete_err(ctx, "missing string result".into()) };
}
}
#[cfg(feature = "async")]
struct StreamContext {
sender: AsyncStreamSender<DownloadStatusUpdate>,
}
#[cfg(feature = "async")]
unsafe extern "C" fn stream_callback(ctx: *mut c_void, event_json: *mut c_char, done: bool) {
if done {
if !ctx.is_null() {
unsafe { drop(Box::from_raw(ctx.cast::<StreamContext>())) };
}
return;
}
if ctx.is_null() || event_json.is_null() {
return;
}
let json = unsafe { ffi::owned_string(event_json) };
let Ok(payload) = serde_json::from_str::<DownloadStatusUpdatePayload>(&json) else {
return;
};
let Ok(update) = DownloadStatusUpdate::try_from(payload) else {
return;
};
let context = unsafe { &mut *ctx.cast::<StreamContext>() };
context.sender.push(update);
}