use crate::blob::{Blob, BlobDigest, BlobIdRef, MediaType};
use crate::core::{Instant, Listing, SimpleSemVer};
use crate::digest::{DigestSha1, DigestSha1FromBytesError, ParseDigestSha1Error};
use crate::game::requests::CreateGameChannel;
use crate::game::store::{
CreateBuild, CreateBuildError, CreateGameChannelError, CreateStoreGame, CreateStoreGameError, GetStoreGame,
GetStoreGameError, GetStoreShortGames, GetStoreShortGamesError, StoreSetGameFavorite, StoreSetGameFavoriteError,
UpdateGameChannelError, UpdateStoreGameChannel,
};
use crate::user::ShortUser;
use arrayvec::ArrayString;
use async_trait::async_trait;
use auto_impl::auto_impl;
use core::fmt;
use etwin_core::core::{LocaleId, PeriodLower};
use etwin_core::user::UserIdRef;
use etwin_core::{declare_new_enum, declare_new_string, declare_new_uuid};
#[cfg(feature = "serde")]
use etwin_serde_tools::{Deserialize, Serialize};
use indexmap::IndexMap;
pub use serde_json::Value as JsonValue;
#[cfg(feature = "sqlx")]
use sqlx::{database, postgres, Database, Postgres};
use std::collections::BTreeMap;
use std::ops::ControlFlow;
use std::str::FromStr;
use thiserror::Error;
pub mod requests {
use crate::core::{BoundedVec, Request};
use crate::game::{
Game, GameChannelKey, GameChannelPatch, GameKey, GameListItem, GameRef, GameRevision, InputGameBuild,
InputGameChannel,
};
use etwin_core::core::{Instant, Listing};
use etwin_core::types::AnyError;
use etwin_core::user::UserIdRef;
#[cfg(feature = "serde")]
use etwin_serde_tools::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GetGames {
pub offset: u32,
pub limit: u32,
pub favorite: bool,
pub time: Option<Instant>,
}
impl Request for GetGames {
/// List of games, with tombstones
///
/// If the user did not have the permission to view the game at time `t`, it
/// is not in the listing.
/// If the user had the permission to the game:
/// - If it still has the permission, game
/// - If it no longer has the permission (or the game was deleted), tombstone
///
/// A user has the permission to view the game if at the time he:
/// - is an admin (Eternalfest group)
/// - is the owner of the game
/// - can view any of the channels
///
/// If the user could view only channel x in the past and now can only view
/// channel y, it results in a situation. Where he can view the game but
/// not the corresponding channels.
type Response = Result<Listing<GameListItem>, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GetGame {
pub game: GameRef,
pub channel: Option<GameChannelKey>,
pub time: Option<Instant>,
}
impl Request for GetGame {
/// Get a game by id or key
type Response = Result<Game, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CreateGame {
pub owner: Option<UserIdRef>,
pub key: Option<GameKey>,
pub build: InputGameBuild,
pub channels: BoundedVec<InputGameChannel, 1, 10>,
}
impl Request for CreateGame {
/// Create a new game
type Response = Result<Game, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CreateGameBuild {
pub game: GameRef,
pub build: InputGameBuild,
}
impl Request for CreateGameBuild {
/// Create a new game build
type Response = Result<GameRevision, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SetGameFavorite {
pub game: GameRef,
pub favorite: bool,
}
impl Request for SetGameFavorite {
type Response = Result<bool, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CreateGameChannel {
pub actor: UserIdRef,
pub game: GameRef,
pub channel: InputGameChannel,
}
impl Request for CreateGameChannel {
/// Create a new game channel
type Response = Result<GameChannelKey, AnyError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UpdateGameChannel {
pub actor: Option<UserIdRef>,
pub game: GameRef,
pub channel_key: GameChannelKey,
// TODO: Versioning token to detect conflicts
pub patches: Vec<GameChannelPatch>,
}
impl Request for UpdateGameChannel {
/// Create a new game
type Response = Result<GameChannelKey, AnyError>;
}
}
pub mod store {
use crate::core::{BoundedVec, Instant};
use crate::game::{GameChannelKey, GameChannelPatch, GameKey, GameRef, InputGameBuild, InputGameChannel};
use etwin_core::types::AnyError;
use etwin_core::user::UserIdRef;
#[cfg(feature = "serde")]
use etwin_serde_tools::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CreateStoreGame {
pub owner: UserIdRef,
pub key: Option<GameKey>,
pub build: InputGameBuild,
pub channels: BoundedVec<InputGameChannel, 1, 10>,
}
#[derive(Debug, thiserror::Error)]
pub enum CreateStoreGameError {
#[error("trying to create a game with an already used key")]
KeyAlreadyInUse,
#[error(transparent)]
Other(#[from] AnyError),
}
impl CreateStoreGameError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CreateBuild {
pub game: GameRef,
/// If `Some(_)`, only create the build if the provided user is an owner of the game
pub if_owner: Option<UserIdRef>,
pub build: InputGameBuild,
}
#[derive(Debug, thiserror::Error)]
pub enum CreateBuildError {
#[error("the provided user reference {0:?} is not an owner of the game")]
NotOwner(UserIdRef),
#[error(transparent)]
Other(#[from] AnyError),
}
impl CreateBuildError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Debug, thiserror::Error)]
pub enum CreateGameChannelError {
#[error(transparent)]
Other(#[from] AnyError),
}
impl CreateGameChannelError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UpdateStoreGameChannel {
pub actor: UserIdRef,
/// If `Some(_)`, only apply the update if the provided user is an owner of the game
pub if_owner: Option<UserIdRef>,
pub game: GameRef,
pub channel_key: GameChannelKey,
// TODO: Versioning token to detect conflicts
pub patches: Vec<GameChannelPatch>,
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateGameChannelError {
#[error("the provided user reference {0:?} is not an owner of the game")]
NotOwner(UserIdRef),
#[error(transparent)]
Other(#[from] AnyError),
}
impl UpdateGameChannelError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GetStoreShortGames {
/// - `Some`: Use user's permissions
/// - `None`: Guest permissions
/// TODO: Expose `system` access (full permissions)
pub actor: Option<UserIdRef>,
pub is_tester: bool,
pub favorite: bool,
pub offset: u32,
pub limit: u32,
pub now: Instant,
pub time: Option<Instant>,
}
#[derive(Debug, thiserror::Error)]
pub enum GetStoreShortGamesError {
#[error(transparent)]
Other(#[from] AnyError),
}
impl GetStoreShortGamesError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GetStoreGame {
pub actor: Option<UserIdRef>,
pub is_tester: bool,
pub now: Instant,
pub time: Option<Instant>,
pub game: GameRef,
pub channel: Option<GameChannelKey>,
}
#[derive(Debug, thiserror::Error)]
pub enum GetStoreGameError {
#[error("revoked view game permssion")]
PermissionRevoked,
#[error(transparent)]
Other(#[from] AnyError),
}
impl GetStoreGameError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreSetGameFavorite {
pub user: UserIdRef,
pub game: GameRef,
pub favorite: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum StoreSetGameFavoriteError {
#[error("revoked view game permssion")]
PermissionRevoked,
#[error(transparent)]
Other(#[from] AnyError),
}
impl StoreSetGameFavoriteError {
pub fn other<E>(e: E) -> Self
where
E: ::std::error::Error + Send + Sync + 'static,
{
Self::Other(Box::new(e))
}
}
}
/// A game (Some) or a tombstone (None).
///
/// In the future, the tombstone may provide more details (game id, reason).
/// In particular the reason should allow to know if it was influenced by
/// the future (e.g. permission revocation) or not.
pub type GameListItem = Option<ShortGame>;
// /// A game represents:
// /// - a set of game channels that may share progress and be
// /// scheduled together
// /// - a set of game versions
// /// - a schedule mapping game versions to channels over time
// /// - an owner
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "Game")
)]
pub struct Game {
pub id: GameId,
pub created_at: Instant,
pub key: Option<GameKey>,
pub owner: ShortUser,
pub channels: GameChannelListing,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GameChannelListing {
pub offset: u32,
pub limit: u32,
pub count: u32,
pub is_count_exact: bool,
pub active: ActiveGameChannel,
pub items: Vec<Option<ShortGameChannel>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreGameChannelListing {
pub offset: u32,
pub limit: u32,
pub count: u32,
pub is_count_exact: bool,
pub active: StoreActiveGameChannel,
pub items: Vec<Option<StoreShortGameChannel>>,
}
impl StoreGameChannelListing {
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
self.active.for_each_blob(&mut f);
for channel in self.items.iter().flatten() {
channel.for_each_blob(&mut f);
}
ControlFlow::Continue(())
}
}
impl Game {
pub const fn as_ref(&self) -> GameIdRef {
GameIdRef::new(self.id)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreGame {
/// Game id
pub id: GameId,
/// Creation time (not necessarily when it was publicly accessible)
pub created_at: Instant,
/// Game key (client controlled name)
pub key: Option<GameKey>,
/// Owner of the game (full management permissions)
pub owner: UserIdRef,
/// The list of channels, restricted to the one with the highest priority
///
/// `channels.limit == 1`
/// `channels.offset` points to the first non-tombstone entry.
/// If it is not `0`, it means that the permissions were changed and you can
/// no longer view the original channel.
pub channels: StoreGameChannelListing,
}
impl StoreGame {
pub fn for_each_blob<F, R>(&self, f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
self.channels.for_each_blob(f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreShortGameChannel {
pub key: GameChannelKey,
pub is_enabled: bool,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
pub sort_update_date: Instant,
pub default_permission: GameChannelPermission,
pub build: StoreShortGameBuild,
}
impl StoreShortGameChannel {
pub fn for_each_blob<F, R>(&self, f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
self.build.for_each_blob(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StoreShortGameBuild {
pub version: SimpleSemVer,
pub git_commit_ref: Option<GitCommitRefSha1>,
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<BlobIdRef>,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, StoreShortGameBuildI18n>,
}
impl StoreShortGameBuild {
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
if let Some(icon) = self.icon {
f(icon)?
}
for v in self.i18n.values() {
v.for_each_blob(&mut f)?;
}
ControlFlow::Continue(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "Game")
)]
pub struct ShortGame {
/// Game id
pub id: GameId,
/// Creation time (not necessarily when it was publicly accessible)
pub created_at: Instant,
/// Game key (client controlled name)
pub key: Option<GameKey>,
/// Owner of the game (full management permissions)
pub owner: ShortUser,
/// The list of channels, restricted to the one with the highest priority
///
/// `channels.limit == 1`
/// `channels.offset` points to the first non-tombstone entry.
/// If it is not `0`, it means that the permissions were changed and you can
/// no longer view the original channel.
pub channels: Listing<ShortGameChannel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "Game")
)]
pub struct StoreShortGame {
/// Game id
pub id: GameId,
/// Creation time (not necessarily when it was publicly accessible)
pub created_at: Instant,
/// Game key (client controlled name)
pub key: Option<GameKey>,
/// Owner of the game (full management permissions)
pub owner: UserIdRef,
/// The list of channels, restricted to the one with the highest priority
///
/// `channels.limit == 1`
/// `channels.offset` points to the first non-tombstone entry.
/// If it is not `0`, it means that the permissions were changed and you can
/// no longer view the original channel.
pub channels: Listing<StoreShortGameChannel>,
}
impl StoreShortGame {
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
for channel in &self.channels.items {
channel.for_each_blob(&mut f)?;
}
ControlFlow::Continue(())
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "GameChannel")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShortGameChannel {
pub key: GameChannelKey,
pub is_enabled: bool,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
pub sort_update_date: Instant,
pub default_permission: GameChannelPermission,
pub build: ShortGameRevision,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameRevision {
pub version: SimpleSemVer,
pub created_at: Instant,
pub git_commit_ref: Option<GitCommitRefSha1>,
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<Blob>,
pub loader: SimpleSemVer,
pub engine: GameEngine,
pub patcher: Option<GamePatcher>,
/// Debug info (e.g. obfuscation map)
pub debug: Option<Blob>,
pub content: Option<Blob>,
pub content_i18n: Option<Blob>,
pub musics: Vec<GameResource>,
pub modes: IndexMap<GameModeKey, GameModeSpec>,
pub families: FamiliesString,
pub category: GameCategory,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, GameBuildI18n>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoreGameBuild {
pub version: SimpleSemVer,
pub created_at: Instant,
pub git_commit_ref: Option<GitCommitRefSha1>,
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<BlobIdRef>,
pub loader: SimpleSemVer,
pub engine: InputGameEngine,
pub patcher: Option<InputGamePatcher>,
pub debug: Option<BlobIdRef>,
pub content: Option<BlobIdRef>,
pub content_i18n: Option<BlobIdRef>,
pub musics: Vec<InputGameResource>,
pub modes: IndexMap<GameModeKey, GameModeSpec>,
pub families: FamiliesString,
pub category: GameCategory,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, InputGameBuildI18n>,
}
impl StoreGameBuild {
pub fn to_short(&self) -> StoreShortGameBuild {
StoreShortGameBuild {
version: self.version,
git_commit_ref: self.git_commit_ref,
main_locale: self.main_locale,
display_name: self.display_name.clone(),
description: self.description.clone(),
icon: self.icon,
i18n: self.i18n.iter().map(|(l, v)| (*l, v.to_store_short())).collect(),
}
}
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
if let Some(icon) = self.icon {
f(icon)?
}
if let InputGameEngine::Custom(engine) = self.engine {
f(engine.blob)?;
}
if let Some(patcher) = &self.patcher {
f(patcher.blob)?;
}
if let Some(debug) = self.debug {
f(debug)?
}
if let Some(content) = self.content {
f(content)?
}
if let Some(content_i18n) = self.content_i18n {
f(content_i18n)?
}
for music in &self.musics {
f(music.blob)?;
}
for v in self.i18n.values() {
v.for_each_blob(&mut f)?;
}
ControlFlow::Continue(())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortGameRevision {
pub version: SimpleSemVer,
pub git_commit_ref: Option<GitCommitRefSha1>,
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<Blob>,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, ShortGameBuildI18n>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InternalStoreGameBuild {
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<BlobIdRef>,
pub loader: SimpleSemVer,
pub engine: InputGameEngine,
pub patcher: Option<InputGamePatcher>,
pub debug: Option<BlobIdRef>,
pub content: Option<BlobIdRef>,
pub content_i18n: Option<BlobIdRef>,
pub musics: Vec<InputGameResource>,
pub modes: IndexMap<GameModeKey, GameModeSpec>,
pub families: FamiliesString,
pub category: GameCategory,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, InputGameBuildI18n>,
}
impl InternalStoreGameBuild {
pub fn to_short(&self) -> InternalStoreShortGameBuild {
InternalStoreShortGameBuild {
main_locale: self.main_locale,
display_name: self.display_name.clone(),
description: self.description.clone(),
icon: self.icon,
i18n: self.i18n.iter().map(|(l, v)| (*l, v.to_store_short())).collect(),
}
}
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
if let Some(icon) = self.icon {
f(icon)?
}
if let InputGameEngine::Custom(engine) = self.engine {
f(engine.blob)?;
}
if let Some(patcher) = &self.patcher {
f(patcher.blob)?;
}
if let Some(debug) = self.debug {
f(debug)?
}
if let Some(content) = self.content {
f(content)?
}
if let Some(content_i18n) = self.content_i18n {
f(content_i18n)?
}
for music in &self.musics {
f(music.blob)?;
}
for v in self.i18n.values() {
v.for_each_blob(&mut f)?;
}
ControlFlow::Continue(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct InternalStoreShortGameBuild {
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<BlobIdRef>,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, StoreShortGameBuildI18n>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InputPeriodLower {
/// If missing, `now`
pub start: Option<Instant>,
/// If missing, +inf
pub end: Option<Instant>,
}
impl InputPeriodLower {
pub const NOW_TO_FOREVER: Self = Self { start: None, end: None };
pub fn resolve_from(self, time: Instant) -> PeriodLower {
let start = self.start.unwrap_or(time);
PeriodLower::new(start, self.end)
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "GameChannel")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActiveGameChannel {
pub key: GameChannelKey,
pub is_enabled: bool,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
pub sort_update_date: Instant,
pub default_permission: GameChannelPermission,
pub build: GameRevision,
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "GameChannel")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoreActiveGameChannel {
pub key: GameChannelKey,
pub is_enabled: bool,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
pub sort_update_date: Instant,
pub default_permission: GameChannelPermission,
pub build: StoreGameBuild,
}
impl StoreActiveGameChannel {
pub fn for_each_blob<F, R>(&self, f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
self.build.for_each_blob(f)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputGameChannel {
pub key: GameChannelKey,
pub is_enabled: bool,
pub default_permission: GameChannelPermission,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
/// `None` is `now`
pub sort_update_date: Option<Instant>,
pub version: SimpleSemVer,
pub patches: Vec<GameChannelPatch>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GameChannelPatch {
pub period: InputPeriodLower,
pub is_enabled: bool,
pub default_permission: GameChannelPermission,
pub is_pinned: bool,
pub publication_date: Option<Instant>,
pub sort_update_date: Instant,
pub version: SimpleSemVer,
}
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(tag = "type", rename = "GameChannel"))]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct GameChannel {
// pub key: GameChannelKey,
// pub display_name: GameChannelDisplayName,
// pub build: GameRevision, // Not all fields are available / based on permissions
// pub permissions: PermissionRules,
// // pub capabilites: ???,
// pub schedule: GameChannelSchedule,
// }
declare_new_uuid! {
pub struct GameId(Uuid);
pub type ParseError = GameIdParseError;
const SQL_NAME = "game_id";
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "Game")
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GameIdRef {
pub id: GameId,
}
impl GameIdRef {
pub const fn new(id: GameId) -> Self {
Self { id }
}
}
impl From<GameId> for GameIdRef {
fn from(id: GameId) -> Self {
Self::new(id)
}
}
declare_new_string! {
pub struct GameKey(String);
pub type ParseError = GameKeyParseError;
const PATTERN = r"^[a-z][a-z_0-9]{0,99}$";
const SQL_NAME = "game_key";
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "type", rename = "Game")
)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GameKeyRef {
pub key: GameKey,
}
impl GameKeyRef {
pub const fn new(key: GameKey) -> Self {
Self { key }
}
}
impl From<GameKey> for GameKeyRef {
fn from(key: GameKey) -> Self {
Self { key }
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GameRef {
Id(GameIdRef),
Key(GameKeyRef),
}
impl GameRef {
pub const fn id(id: GameId) -> Self {
Self::Id(GameIdRef::new(id))
}
pub const fn key(key: GameKey) -> Self {
Self::Key(GameKeyRef::new(key))
}
pub const fn split(&self) -> (Option<GameIdRef>, Option<&GameKeyRef>) {
let mut id: Option<GameIdRef> = None;
let mut key: Option<&GameKeyRef> = None;
match self {
Self::Id(r) => id = Some(*r),
Self::Key(r) => key = Some(r),
};
(id, key)
}
pub const fn split_deref(&self) -> (Option<GameId>, Option<&GameKey>) {
let mut id: Option<GameId> = None;
let mut key: Option<&GameKey> = None;
match self {
Self::Id(r) => id = Some(r.id),
Self::Key(r) => key = Some(&r.key),
};
(id, key)
}
}
impl From<&'_ Game> for GameRef {
fn from(game: &Game) -> Self {
Self::Id(game.as_ref())
}
}
impl From<GameIdRef> for GameRef {
fn from(id: GameIdRef) -> Self {
Self::Id(id)
}
}
impl From<GameId> for GameRef {
fn from(id: GameId) -> Self {
Self::Id(id.into())
}
}
impl From<GameKeyRef> for GameRef {
fn from(key: GameKeyRef) -> Self {
Self::Key(key)
}
}
impl From<GameKey> for GameRef {
fn from(key: GameKey) -> Self {
Self::Key(key.into())
}
}
#[derive(Debug, Error)]
#[error("invalid forum section id ({id}) or key ({key})")]
pub struct GameRefParseError {
pub id: GameIdParseError,
pub key: GameKeyParseError,
}
impl FromStr for GameRef {
type Err = GameRefParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match GameId::from_str(s) {
Ok(r) => Ok(r.into()),
Err(id) => match GameKey::from_str(s) {
Ok(r) => Ok(r.into()),
Err(key) => Err(GameRefParseError { id, key }),
},
}
}
}
declare_new_string! {
pub struct GameDisplayName(String);
pub type ParseError = GameDisplayNameError;
const PATTERN = r"^\S(?:.{0,62}\S)?$";
const SQL_NAME = "game_display_name";
}
declare_new_string! {
pub struct GameDescription(String);
pub type ParseError = GameDescriptionError;
const PATTERN = r"^\S(?s:.{0,998}\S)?$";
const SQL_NAME = "game_description";
}
declare_new_string! {
pub struct GameChannelKey(String);
pub type ParseError = GameChannelKeyParseError;
const PATTERN = r"^[a-z_][a-z_0-9]{0,99}$";
const SQL_NAME = "game_channel_key";
}
// declare_new_string! {
// pub struct GameChannelDisplayName(String);
// pub type ParseError = GameChannelDisplayNameError;
// const PATTERN = r"^\S(?:.{0,62}\S)?$";
// const SQL_NAME = "game_channel_display_name";
// }
declare_new_string! {
pub struct GameOptionDisplayName(String);
pub type ParseError = GameOptionDisplayNameError;
const PATTERN = r"^\S(?:.{0,62}\S)?$";
const SQL_NAME = "game_option_display_name";
}
declare_new_string! {
pub struct GameOptionKey(String);
pub type ParseError = GameOptionKeyParseError;
const PATTERN = r"^[a-z_][a-z_0-9]{0,99}$";
const SQL_NAME = "game_option_key";
}
declare_new_string! {
pub struct GameModeDisplayName(String);
pub type ParseError = GameModeDisplayNameError;
const PATTERN = r"^\S(?:.{0,62}\S)?$";
const SQL_NAME = "game_mode_display_name";
}
declare_new_string! {
pub struct GameModeKey(String);
pub type ParseError = GameModeKeyParseError;
const PATTERN = r"^[a-z_][a-z_0-9]{0,99}$";
const SQL_NAME = "game_mode_key";
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(try_from = "&str", into = "ArrayString<40>")
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GitCommitRefSha1(DigestSha1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
#[error(transparent)]
pub struct GitCommitRefSha1FromBytesError(#[from] pub DigestSha1FromBytesError);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
#[error(transparent)]
pub struct ParseGitCommitRefSha1Error(#[from] pub ParseDigestSha1Error);
impl GitCommitRefSha1 {
pub fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, GitCommitRefSha1FromBytesError> {
Ok(Self(DigestSha1::from_bytes(bytes)?))
}
pub fn from_hex(input: &str) -> Result<Self, ParseGitCommitRefSha1Error> {
Ok(Self(DigestSha1::from_hex(input)?))
}
pub fn hex(&self) -> ArrayString<40> {
self.0.hex()
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<Postgres> for GitCommitRefSha1 {
fn type_info() -> postgres::PgTypeInfo {
postgres::PgTypeInfo::with_name("git_commit_ref")
}
fn compatible(ty: &postgres::PgTypeInfo) -> bool {
*ty == Self::type_info() || <&[u8] as sqlx::Type<Postgres>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'r, Db: Database> sqlx::Decode<'r, Db> for GitCommitRefSha1
where
&'r [u8]: sqlx::Decode<'r, Db>,
{
fn decode(
value: <Db as database::HasValueRef<'r>>::ValueRef,
) -> Result<GitCommitRefSha1, Box<dyn std::error::Error + 'static + Send + Sync>> {
let value: &[u8] = <&[u8] as sqlx::Decode<Db>>::decode(value)?;
Ok(GitCommitRefSha1::from_bytes(value)?)
}
}
#[cfg(feature = "sqlx")]
impl<'q, Db: Database> sqlx::Encode<'q, Db> for GitCommitRefSha1
where
Vec<u8>: sqlx::Encode<'q, Db>,
{
fn encode_by_ref(&self, buf: &mut <Db as database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.as_slice().to_vec().encode(buf)
}
}
impl TryFrom<&[u8]> for GitCommitRefSha1 {
type Error = GitCommitRefSha1FromBytesError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl TryFrom<&str> for GitCommitRefSha1 {
type Error = ParseGitCommitRefSha1Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_hex(value)
}
}
impl From<GitCommitRefSha1> for ArrayString<40> {
fn from(value: GitCommitRefSha1) -> Self {
value.hex()
}
}
impl FromStr for GitCommitRefSha1 {
type Err = ParseGitCommitRefSha1Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::from_hex(input)
}
}
impl fmt::Display for GitCommitRefSha1 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.hex().fmt(f)
}
}
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct PermissionRuleset {
// default: HashSet<GameChannelPermission>,
// rules: HashMap<GameChannelActor, HashSet<GameChannelPermission>>,
// }
declare_new_enum!(
/// Permissions for a game channel:
/// - None: No permission at all (can't even see the channel)
/// - View: View the channel, but can't start runs
/// - Play: Start regular runs and save results
/// - Debug: Start debug runs
/// - Manage: Full permissions, update the channel schedule, etc.
pub enum GameChannelPermission {
#[str("None")]
None,
#[str("View")]
View,
#[str("Play")]
Play,
#[str("Debug")]
Debug,
#[str("Manage")]
Manage,
}
pub type ParseError = GameChannelPermissionParseError;
const SQL_NAME = "game_channel_permission";
);
declare_new_enum!(
pub enum GameCategory {
#[str("Big")]
Big,
#[str("Small")]
Small,
#[str("Puzzle")]
Puzzle,
#[str("Challenge")]
Challenge,
#[str("Fun")]
Fun,
#[str("Lab")]
Lab,
#[str("Other")]
Other,
}
pub type ParseError = GameCategoryParseError;
// TODO: Rename to `game_category`
const SQL_NAME = "game_category2";
);
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
// pub enum GameChannelActor {
// /// Specific user
// User(UserId),
// /// Members of the `Eternalfest` group
// Eternalfest,
// }
//
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct GameChannelSchedule {
// pub current: Option<GameChannelScheduleItem>,
// pub items: Listing<GameChannelScheduleItem>,
// }
//
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct GameChannelScheduleItem {
// pub build: GameRevision,
// // TODO: pub periode: PeriodFrom,
// pub start: Instant,
// pub end: Option<Instant>,
// // /// This a major release: make it stand out.
// // pub major: bool,
// }
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GameVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputGameBuild {
pub version: SimpleSemVer,
pub git_commit_ref: Option<GitCommitRefSha1>,
pub main_locale: LocaleId,
pub display_name: GameDisplayName,
pub description: GameDescription,
pub icon: Option<BlobIdRef>,
pub loader: SimpleSemVer,
pub engine: InputGameEngine,
pub patcher: Option<InputGamePatcher>,
/// Debug info (e.g. obfuscation map)
pub debug: Option<BlobIdRef>,
pub content: Option<BlobIdRef>,
pub content_i18n: Option<BlobIdRef>,
pub musics: Vec<InputGameResource>,
pub modes: IndexMap<GameModeKey, GameModeSpec>,
pub families: FamiliesString,
pub category: GameCategory,
#[cfg_attr(feature = "serde", serde(default))]
pub i18n: BTreeMap<LocaleId, InputGameBuildI18n>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameBuildI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameDisplayName>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<GameDescription>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<Blob>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content_i18n: Option<Blob>,
#[cfg_attr(feature = "serde", serde(default))]
pub modes: BTreeMap<GameModeKey, GameModeSpecI18n>,
}
impl GameBuildI18n {
pub fn to_short(&self) -> ShortGameBuildI18n {
ShortGameBuildI18n {
display_name: self.display_name.clone(),
description: self.description.clone(),
icon: self.icon.clone(),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ShortGameBuildI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameDisplayName>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<GameDescription>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<Blob>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InputGameBuildI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameDisplayName>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<GameDescription>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<BlobIdRef>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content_i18n: Option<BlobIdRef>,
#[cfg_attr(feature = "serde", serde(default))]
pub modes: BTreeMap<GameModeKey, GameModeSpecI18n>,
}
impl InputGameBuildI18n {
pub fn to_store_short(&self) -> StoreShortGameBuildI18n {
StoreShortGameBuildI18n {
display_name: self.display_name.clone(),
description: self.description.clone(),
icon: self.icon,
}
}
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
if let Some(icon) = self.icon {
f(icon)?
}
if let Some(content_i18n) = self.content_i18n {
f(content_i18n)?
}
ControlFlow::Continue(())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StoreShortGameBuildI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameDisplayName>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<GameDescription>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<BlobIdRef>,
}
impl StoreShortGameBuildI18n {
pub fn for_each_blob<F, R>(&self, mut f: F) -> ControlFlow<R>
where
F: FnMut(BlobIdRef) -> ControlFlow<R>,
{
if let Some(icon) = self.icon {
f(icon)?
}
ControlFlow::Continue(())
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameModeSpec {
pub display_name: GameModeDisplayName,
pub is_visible: bool,
pub options: IndexMap<GameOptionKey, GameOptionSpec>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GameModeSpecI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameModeDisplayName>,
#[cfg_attr(feature = "serde", serde(default))]
pub options: BTreeMap<GameOptionKey, GameOptionSpecI18n>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameOptionSpec {
pub display_name: GameOptionDisplayName,
pub is_visible: bool,
pub is_enabled: bool,
pub default_value: bool,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GameOptionSpecI18n {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<GameOptionDisplayName>,
}
// #[cfg_attr(
// feature = "serde",
// derive(Serialize, Deserialize),
// serde(tag = "type", rename = "GameRevision")
// )]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct InputGameRevision {
// pub main_locale: LocaleId,
// pub display_name: GameDisplayName,
// pub description: GameDescription,
// pub icon: Option<GameFile>,
// }
// #[cfg_attr(
// feature = "serde",
// derive(Serialize, Deserialize),
// serde(tag = "type", rename = "GameRevision")
// )]
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub struct GameRevision {
// pub id: GameRevisionId,
// pub main_locale: LocaleId,
// pub display_name: Option<GameDisplayName>,
// pub description: Option<GameDescription>,
// pub icon: Option<GameFile>,
// pub engine: GameEngine,
// pub patcher: Option<GamePatcher>,
// pub content: Option<GameContent>,
// pub messages: Option<GameFile>,
// pub musics: BoundedVec<GameFile, 0, 100>,
// // Custom extra files
// pub files: BoundedVec<GameFile, 0, 100>,
// // pub game_modes: GameModeSet,
// pub state_spec: GameStateSpec,
// // pub debug: Option<GameDebug>,
// // pub state_rules: GameStateRules,
// }
declare_new_uuid! {
pub struct GameRevisionId(Uuid);
pub type ParseError = GameRevisionIdParseError;
const SQL_NAME = "game_build_id";
}
// pub struct GameDebug {
// pub obfu_map: GameFile,
// }
//
// pub struct GameModeSet(HashMap<String, GameMode>);
//
// pub struct GameOptionGroup {
// inner: Vec<GameOption>,
// }
//
// pub struct GameOption {
// key: String,
// }
//
// pub enum GameOptionKind {
// Bool { default: bool },
// Select { values: Vec1<GameSelectOptionValue> },
// // TODOD: non-exhaustive
// }
//
// pub struct GameSelectOptionValue {
// key: String,
// display: String,
// locales: HashMap<LocaleId, String>,
// }
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(tag = "type"))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GameEngine {
/// Use Motion-Twin's latest game engine (version 96).
V96,
/// Use a custom game engine.
Custom(CustomGameEngine),
}
impl GameEngine {
pub const fn custom(blob: Blob) -> Self {
Self::Custom(CustomGameEngine { blob })
}
pub fn as_custom(&self) -> Option<&CustomGameEngine> {
match self {
Self::V96 => None,
Self::Custom(engine) => Some(engine),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CustomGameEngine {
pub blob: Blob,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(tag = "type"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InputGameEngine {
/// Use Motion-Twin's latest game engine (version 96).
V96,
/// Use a custom game engine.
Custom(InputCustomGameEngine),
}
impl InputGameEngine {
pub const fn custom(blob: BlobIdRef) -> Self {
Self::Custom(InputCustomGameEngine { blob })
}
}
declare_new_string! {
pub struct GameResourceDisplayName(String);
pub type ParseError = GameFileDisplayNameError;
// language=regexp
const PATTERN = r"^\S(?:.{0,98}\S)?$";
const SQL_NAME = "game_resource_display_name";
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InputCustomGameEngine {
pub blob: BlobIdRef,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GamePatcher {
pub blob: Blob,
pub framework: PatcherFramework,
/// Optional metadata about this build (e.g. dependencies)
pub meta: Option<JsonValue>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputGamePatcher {
pub blob: BlobIdRef,
pub framework: PatcherFramework,
/// Optional metadata about this build (e.g. dependencies)
pub meta: Option<JsonValue>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PatcherFramework {
pub name: PatcherFrameworkName,
pub version: SimpleSemVer,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GameResource {
pub blob: Blob,
pub display_name: Option<GameResourceDisplayName>,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InputGameResource {
pub blob: BlobIdRef,
pub display_name: Option<GameResourceDisplayName>,
}
/// Game assets have deduplicated key, but the download URL must go through a game channel for permissions
/// /api/games/<gameId>/channel/<channelId>/assets/<key>/raw
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GameAsset {
/// The key is generated by the server based on data
pub key: String,
pub media_type: MediaType,
pub byte_size: u32,
pub digest: BlobDigest,
pub display_name: Option<GameResourceDisplayName>,
}
declare_new_string! {
pub struct FamiliesString(String);
pub type ParseError = FamiliesStringError;
const PATTERN = r"^(?:(?:0|[1-9]\d*)(?:,(?:0|[1-9]\d*))*)?$";
const SQL_NAME = "families_string";
}
declare_new_string! {
pub struct PatcherFrameworkName(String);
pub type ParseError = PatcherFrameworkNameError;
const PATTERN = r"^[a-z][a-z0-9]{0,31}$";
const SQL_NAME = "patcher_framework_name";
}
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
// pub struct GamePatcherFramework {
// name: String,
// version: Option<SemVer>,
// }
//
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
// pub enum GameContent {
// Xml(GameFile),
// }
//
// /// The game state specification describes how state is handled for games.
// ///
// /// State corresponds to input received on run start and output stored on
// /// game end.
// /// There are three steps:
// /// 1. Output is stored per-run
// /// 2. Input is generated by aggregating stats from previous runs
// /// 3. Migrations describe how to handle stats from other revisions in this
// /// channel.
// ///
// /// Items and quests are special-cased stats.
// /// - Items:
// /// - Run: integer counts
// /// - Aggregated: Sum.
// /// - Quests:
// /// - Run: nothing
// /// - Aggregated: Resolved requirement/reward graph
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
// pub struct GameStateSpec {
// pub items: Vec<GameItemMeta>,
// pub quests: GameQuestsSpec,
// pub stats: RunStatsSchema,
// pub migrations: GameStateMigrations,
// }
//
// declare_new_uuid! {
// pub struct GameItemMeta(Uuid);
// pub type ParseError = GameItemMetaParseError;
// const SQL_NAME = "game_revision_id";
// }
//
// declare_new_uuid! {
// pub struct GameQuestsSpec(Uuid);
// pub type ParseError = GameQuestsSpecParseError;
// const SQL_NAME = "game_revision_id";
// }
//
// declare_new_uuid! {
// pub struct RunStatsSchema(Uuid);
// pub type ParseError = RunStatsSchemaParseError;
// const SQL_NAME = "game_revision_id";
// }
//
// declare_new_uuid! {
// pub struct GameStateMigrations(Uuid);
// pub type ParseError = GameStateMigrationsParseError;
// const SQL_NAME = "game_revision_id";
// }
//
// /// Inputs:
// /// - Monotonic stats:
// /// - quest(<id>): `false` then `true` once unlocked
// struct ProgressSolver {}
//
// declare_new_uuid! {
// pub struct GameItemFamilyId(Uuid);
// pub type ParseError = GameItemFamilyIdParseError;
// const SQL_NAME = "game_item_family_id";
// }
//
// pub struct GameItemFamily {
// pub id: GameItemFamilyId,
// pub local_id: u32,
// pub name: u32,
// }
//
// struct Quest {
// pub id: Uuid,
// pub key: String,
// pub hidden: bool,
// pub requirements: Vec<QuestRequirement>,
// pub reward: QuestReward,
// }
//
// pub enum QuestRequirementLabel {
// Item(LocalItemId),
// Text(String),
// }
//
// /// Quest requirement based on the total number of items
// pub struct ItemQuestRequirement {
// pub label: String,
// pub target: u32,
// }
//
// pub struct QuestRequirement {
// pub title: String,
// pub target: u32,
// }
//
// pub struct QuestReward {
// pub message: String,
// }
//
// pub struct QuestRewardLocale {
// pub message: String,
// }
//
// // pub enum QuestRequirement {
// // ItemCount,
// // }
// //
// // pub enum QuestRequirementTarget {
// // LessThanOr,
// // Max,
// // }
//
// // pub struct GameItem {
// // pub id: Uuid,
// // pub local_id: u32,
// // pub name: String,
// // pub image: GameItemImage,
// // pub locale: HashMap<LocaleId, GameItemLocale>,
// // }
// //
// // pub struct GameItemImage {
// // pub raw: GameFile,
// // }
// //
// // pub struct GameItemLocale {
// // pub name: Option<String>,
// // pub image: Option<GameItemImage>,
// // }
// //
// // pub struct RunStatsSchema {
// // /// The first one is the root type, others use bounds...
// // inner: BoundedVec<StatType, 1, 100>,
// // }
// //
// // pub enum StatType {
// // Sint32,
// // Float64,
// // Bool,
// // Duration,
// // String,
// // Record(StatRecord),
// // Enum(StatEnum),
// // Vec(u32),
// // }
// //
// // pub struct StatRecord {
// // properties: HashMap<String, u32>,
// // }
// //
// // pub struct StatEnum {
// // variants: HashMap<String, u32>,
// // }
#[async_trait]
#[auto_impl(&, Arc)]
pub trait GameStore: Send + Sync {
async fn create_game(&self, options: &CreateStoreGame) -> Result<StoreGame, CreateStoreGameError>;
async fn create_version(&self, options: &CreateBuild) -> Result<StoreGameBuild, CreateBuildError>;
async fn create_game_channel(&self, options: &CreateGameChannel) -> Result<GameChannelKey, CreateGameChannelError>;
async fn update_game_channel(
&self,
options: &UpdateStoreGameChannel,
) -> Result<(StoreActiveGameChannel, Instant), UpdateGameChannelError>;
async fn get_short_games(
&self,
options: &GetStoreShortGames,
) -> Result<Listing<Option<StoreShortGame>>, GetStoreShortGamesError>;
async fn get_game(&self, options: &GetStoreGame) -> Result<StoreGame, GetStoreGameError>;
async fn set_game_favorite(&self, options: &StoreSetGameFavorite) -> Result<bool, StoreSetGameFavoriteError>;
}
#[cfg(test)]
mod test {
use crate::game::requests::CreateGame;
use crate::game::InputGameBuild;
use std::fs;
mod create_game {
mod sous_la_colline {
use super::super::*;
use crate::blob::BlobIdRef;
use crate::core::BoundedVec;
use crate::game::{
GameCategory, GameChannelPermission, GameModeSpec, GameModeSpecI18n, GameOptionSpec, GameOptionSpecI18n,
InputGameBuildI18n, InputGameChannel, InputGameEngine,
};
use etwin_core::core::LocaleId;
use etwin_core::user::UserIdRef;
fn value() -> CreateGame {
CreateGame {
owner: Some(UserIdRef::new("00000000-0000-0000-0001-000000000001".parse().unwrap())),
key: Some("hill".parse().unwrap()),
build: InputGameBuild {
version: "1.2.3".parse().unwrap(),
git_commit_ref: Some("0123456789abcdef0123456789abcdef01234567".parse().unwrap()),
main_locale: LocaleId::FrFr,
display_name: "Sous la colline".parse().unwrap(),
description: "Contrée emblématique d'Eternalfest qui ravira les débutants et nostalgiques\u{202f}!"
.parse()
.unwrap(),
icon: Some(BlobIdRef::new("00000000-0000-0000-0002-000000000001".parse().unwrap())),
loader: "4.1.0".parse().unwrap(),
engine: InputGameEngine::V96,
patcher: None,
debug: None,
content: None,
content_i18n: Some(BlobIdRef::new("00000000-0000-0000-0002-000000000002".parse().unwrap())),
musics: vec![],
modes: [
(
"solo".parse().unwrap(),
GameModeSpec {
display_name: "Aventure".parse().unwrap(),
is_visible: true,
options: [(
"boost".parse().unwrap(),
GameOptionSpec {
display_name: "Tornade".parse().unwrap(),
is_visible: true,
is_enabled: true,
default_value: false,
},
)]
.into_iter()
.collect(),
},
),
(
"multi".parse().unwrap(),
GameModeSpec {
display_name: "Multicoopératif".parse().unwrap(),
is_visible: true,
options: [(
"lifesharing".parse().unwrap(),
GameOptionSpec {
display_name: "Partage de vies".parse().unwrap(),
is_visible: true,
is_enabled: true,
default_value: false,
},
)]
.into_iter()
.collect(),
},
),
]
.into_iter()
.collect(),
families: "10".parse().unwrap(),
category: GameCategory::Small,
i18n: [(
LocaleId::EnUs,
InputGameBuildI18n {
display_name: Some("Under the hill".parse().unwrap()),
description: Some(
"Hallmark Eternalfest game for beginners and nostalgics\u{202f}!"
.parse()
.unwrap(),
),
icon: None,
content_i18n: Some(BlobIdRef::new("00000000-0000-0000-0002-000000000002".parse().unwrap())),
modes: [
(
"solo".parse().unwrap(),
GameModeSpecI18n {
display_name: Some("Adventure".parse().unwrap()),
options: [(
"boost".parse().unwrap(),
GameOptionSpecI18n {
display_name: Some("Tornado".parse().unwrap()),
},
)]
.into_iter()
.collect(),
},
),
(
"multi".parse().unwrap(),
GameModeSpecI18n {
display_name: Some("Multiplayer".parse().unwrap()),
options: [(
"lifesharing".parse().unwrap(),
GameOptionSpecI18n {
display_name: Some("Life sharing".parse().unwrap()),
},
)]
.into_iter()
.collect(),
},
),
]
.into_iter()
.collect(),
},
)]
.into_iter()
.collect(),
},
channels: BoundedVec::new(vec![InputGameChannel {
key: "main".parse().unwrap(),
is_enabled: false,
default_permission: GameChannelPermission::None,
is_pinned: false,
publication_date: None,
sort_update_date: None,
version: "1.2.3".parse().unwrap(),
patches: vec![],
}])
.unwrap(),
}
}
#[cfg(feature = "serde")]
#[test]
fn read() {
let s = fs::read_to_string("../../test-resources/core/game/create-game/sous-la-colline/value.json").unwrap();
let actual: CreateGame = serde_json::from_str(&s).unwrap();
let expected = value();
assert_eq!(actual, expected);
}
#[cfg(feature = "serde")]
#[test]
fn write() {
let value = value();
let actual: String = serde_json::to_string_pretty(&value).unwrap();
let expected =
fs::read_to_string("../../test-resources/core/game/create-game/sous-la-colline/value.json").unwrap();
assert_eq!(&actual, expected.trim());
}
}
}
mod game {
mod beta {
use super::super::*;
use crate::game::{
ActiveGameChannel, Game, GameCategory, GameChannelListing, GameChannelPermission, GameEngine, GameId,
GameRevision, ShortGameChannel, ShortGameRevision,
};
use crate::user::ShortUser;
use etwin_core::core::{Instant, LocaleId};
use etwin_core::user::UserId;
use std::collections::BTreeMap;
use std::str::FromStr;
fn value() -> Game {
Game {
id: GameId::from_str("00000000-0000-0000-0003-000000000002").unwrap(),
created_at: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
key: None,
owner: ShortUser {
id: UserId::from_str("00000000-0000-0000-0001-000000000001").unwrap(),
display_name: "Alice".parse().unwrap(),
},
channels: GameChannelListing {
offset: 0,
limit: 10,
count: 3,
is_count_exact: false,
active: ActiveGameChannel {
key: "beta".parse().unwrap(),
is_enabled: true,
is_pinned: false,
publication_date: None,
sort_update_date: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
default_permission: GameChannelPermission::None,
build: GameRevision {
version: "0.1.2".parse().unwrap(),
created_at: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
git_commit_ref: None,
main_locale: LocaleId::FrFr,
display_name: "Beta".parse().unwrap(),
description: "Beta game".parse().unwrap(),
icon: None,
loader: "4.1.0".parse().unwrap(),
engine: GameEngine::V96,
patcher: None,
debug: None,
content: None,
content_i18n: None,
musics: vec![],
modes: Default::default(),
families: "1,2,3".parse().unwrap(),
category: GameCategory::Big,
i18n: BTreeMap::new(),
},
},
items: vec![
None,
Some(ShortGameChannel {
key: "beta".parse().unwrap(),
is_enabled: true,
is_pinned: false,
publication_date: None,
sort_update_date: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
default_permission: GameChannelPermission::None,
build: ShortGameRevision {
version: "0.1.2".parse().unwrap(),
git_commit_ref: None,
main_locale: LocaleId::FrFr,
display_name: "Beta".parse().unwrap(),
description: "Beta game".parse().unwrap(),
icon: None,
i18n: BTreeMap::new(),
},
}),
Some(ShortGameChannel {
key: "dev".parse().unwrap(),
is_enabled: true,
is_pinned: false,
publication_date: None,
sort_update_date: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
default_permission: GameChannelPermission::None,
build: ShortGameRevision {
version: "0.1.2".parse().unwrap(),
git_commit_ref: None,
main_locale: LocaleId::FrFr,
display_name: "Beta".parse().unwrap(),
description: "Beta game".parse().unwrap(),
icon: None,
i18n: BTreeMap::new(),
},
}),
],
},
}
}
#[cfg(feature = "serde")]
#[test]
fn read() {
let s = fs::read_to_string("../../test-resources/core/game/game/beta/value.json").unwrap();
let actual: Game = serde_json::from_str(&s).unwrap();
let expected = value();
assert_eq!(actual, expected);
}
#[cfg(feature = "serde")]
#[test]
fn write() {
let value = value();
let actual: String = serde_json::to_string_pretty(&value).unwrap();
let expected = fs::read_to_string("../../test-resources/core/game/game/beta/value.json").unwrap();
assert_eq!(&actual, expected.trim());
}
}
}
mod short_game_listing {
mod alpha_beta {
use super::super::*;
use crate::core::Listing;
use crate::game::{GameChannelPermission, GameId, ShortGame, ShortGameChannel, ShortGameRevision};
use crate::user::ShortUser;
use etwin_core::core::{Instant, LocaleId};
use etwin_core::user::UserId;
use std::collections::BTreeMap;
use std::str::FromStr;
fn value() -> Listing<Option<ShortGame>> {
Listing {
offset: 0,
limit: 10,
count: 3,
is_count_exact: false,
items: vec![
Some(ShortGame {
id: GameId::from_str("00000000-0000-0000-0003-000000000001").unwrap(),
created_at: Instant::ymd_hms(2022, 1, 1, 0, 0, 1),
key: None,
owner: ShortUser {
id: UserId::from_str("00000000-0000-0000-0001-000000000001").unwrap(),
display_name: "Alice".parse().unwrap(),
},
channels: Listing {
offset: 0,
limit: 1,
count: 2,
is_count_exact: false,
items: vec![ShortGameChannel {
key: "main".parse().unwrap(),
is_enabled: true,
is_pinned: false,
publication_date: None,
sort_update_date: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
default_permission: GameChannelPermission::None,
build: ShortGameRevision {
version: "1.2.3".parse().unwrap(),
git_commit_ref: None,
main_locale: LocaleId::FrFr,
display_name: "Alpha".parse().unwrap(),
description: "Alpha game".parse().unwrap(),
icon: None,
i18n: BTreeMap::new(),
},
}],
},
}),
Some(ShortGame {
id: GameId::from_str("00000000-0000-0000-0003-000000000002").unwrap(),
created_at: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
key: None,
owner: ShortUser {
id: UserId::from_str("00000000-0000-0000-0001-000000000001").unwrap(),
display_name: "Alice".parse().unwrap(),
},
channels: Listing {
offset: 1,
limit: 1,
count: 3,
is_count_exact: false,
items: vec![ShortGameChannel {
key: "beta".parse().unwrap(),
is_enabled: true,
is_pinned: false,
publication_date: None,
sort_update_date: Instant::ymd_hms(2022, 1, 1, 0, 0, 2),
default_permission: GameChannelPermission::None,
build: ShortGameRevision {
version: "0.1.2".parse().unwrap(),
git_commit_ref: None,
main_locale: LocaleId::FrFr,
display_name: "Beta".parse().unwrap(),
description: "Beta game".parse().unwrap(),
icon: None,
i18n: BTreeMap::new(),
},
}],
},
}),
None,
],
}
}
#[cfg(feature = "serde")]
#[test]
fn read() {
let s = fs::read_to_string("../../test-resources/core/game/short-game-listing/alpha-beta/value.json").unwrap();
let actual: Listing<Option<ShortGame>> = serde_json::from_str(&s).unwrap();
let expected = value();
assert_eq!(actual, expected);
}
#[cfg(feature = "serde")]
#[test]
fn write() {
let value = value();
let actual: String = serde_json::to_string_pretty(&value).unwrap();
let expected =
fs::read_to_string("../../test-resources/core/game/short-game-listing/alpha-beta/value.json").unwrap();
assert_eq!(&actual, expected.trim());
}
}
}
mod game_description {
mod ololzd2 {
use super::super::*;
use crate::game::GameDescription;
use std::str::FromStr;
fn value() -> GameDescription {
GameDescription::from_str("Suite à la contrée au succès mondial et acclamée par toutes les critiques, découvrez Ololzd2, la suite ambitieuse qui va causer débats et controverse sur le lore !\nVoyagez avec Igor à travers les différentes timelines du monde d'Ololzd pour comprendre quels sont les événements qui ont conduit aux conflits observés dans Ololzd.\n\nArriverez-vous à recréer le sceau d'Ololzd pour prévenir le cataclysme qui a englouti Ololzd, ou échouerez-vous dans cette quête, ce qui lancera ce monde dans la course inévitable qui amènera à sa perte ?\n\nSeul vous êtes capable d'une telle prouesse... Il est désormais temps de braver le terrible labyrinthe d'Ololzd2 !\n\n(Ou vous pouvez appuyer sur la touche 'k' si la pression d'une telle mission est trop grande)").unwrap()
}
#[cfg(feature = "serde")]
#[test]
fn read() {
let s = fs::read_to_string("../../test-resources/core/game/game-description/ololzd2/value.json").unwrap();
let actual: GameDescription = serde_json::from_str(&s).unwrap();
let expected = value();
assert_eq!(actual, expected);
}
#[cfg(feature = "serde")]
#[test]
fn write() {
let value = value();
let actual: String = serde_json::to_string_pretty(&value).unwrap();
let expected =
fs::read_to_string("../../test-resources/core/game/game-description/ololzd2/value.json").unwrap();
assert_eq!(&actual, expected.trim());
}
}
}
}