Header menu logo Mibo

Assets (loading + caching)

Mibo exposes a per-game asset service (IAssets) that wraps MonoGame’s ContentManager plus a simple cache.

What it provides:

Enabling the service

Add it in your composition root:

Program.mkProgram init update
|> Program.withAssets

Then use the helpers from Mibo.Elmish.Assets anywhere you have a GameContext:

let init (ctx: GameContext): struct(Model * Cmd<Msg>) =
  let player = Assets.texture "sprites/player" ctx
  let font = Assets.font "fonts/ui" ctx
  let shader = Assets.effect "Effects/Grid" ctx
  { PlayerTex = player; Font = font; Shader = shader }, Cmd.none

Custom assets (GPU-backed or not)

You can cache custom values (GPU-backed or not):

let outlineFx =
  Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx

JSON helpers

Mibo includes JSON helpers via JDeck:

These read JSON files from disk and optionally cache them for the game lifetime.

If you’re writing your own decoders, see the JDeck docs:

Building your own “store” on top of IAssets

IAssets is a convenient place to centralize loading + reuse.

For game-specific data sets (skills, items, quests, dialogue, etc), it can be useful to wrap IAssets in a specialized store that:

Example: SkillStore (JSON)

open Mibo.Elmish
open JDeck

type SkillId = SkillId of string

type Skill = {
  Id: SkillId
  Name: string
  CooldownSeconds: float32
}

module SkillDecoders =
  let skillDecoder : JDeck.Decoder<Skill> =
    fun json -> JDeck.decode {
      let! id = json |> Required.Property.get("id", Required.string)
      let! name = json |> Required.Property.get("name", Required.string)
      let! cd = json |> Required.Property.get("cooldownSeconds", Required.float32)
      return {
        Id = SkillId id
        Name = name
        CooldownSeconds = cd
      }
    }

type SkillStore(assets: IAssets) =
  /// Load a skill list (no caching). Useful if you want to manage lifetime yourself.
  member _.LoadAll(path: string) : Skill list =
    Assets.fromJson path (Required.list SkillDecoders.skillDecoder)

  /// Load and cache (game-lifetime). Useful for static data.
  member _.LoadAllCached(path: string, ctx: GameContext) : Skill list =
    Assets.fromJsonCache path (Required.list SkillDecoders.skillDecoder) ctx

  /// A typed cache for an indexed view.
  member _.IndexByIdCached(path: string, ctx: GameContext) : Map<SkillId, Skill> =
    Assets.getOrCreate ("SkillStore/IndexById/" + path)
      (fun _gd ->
        let skills = Assets.fromJson path (Required.list SkillDecoders.skillDecoder)
        skills |> List.map (fun s -> s.Id, s) |> Map.ofList)
      ctx

You can create the store from context:

let store = SkillStore(Assets.getService ctx)
let skillsById = store.IndexByIdCached("Content/skills.json", ctx)

Example: SkillStore (SQLite)

If your source of truth is a database, the same idea applies: the store is where you choose I/O strategy and caching.

type SkillStoreDb(conn: System.Data.IDbConnection) =
  member _.GetById(id: SkillId) : Skill option =
    // query, map rows -> Skill
    None

In this case you typically keep caching inside the store (LRU, size cap, explicit invalidation) instead of using IAssets as a forever-cache.

Cache growth and lifetime

The built-in IAssets caches are designed for game-lifetime reuse:

If memory usage is a concern, prefer one of these patterns:

  1. Use non-cached loads (Assets.fromJson) and manage lifetime in your own store.
  2. Build a store with an explicit cap/eviction strategy (LRU) and keep only hot entries.
  3. Structure data so it loads in “chunks” (per level/biome), and keep chunk lifetime separate from the global IAssets cache.

Lifetime and disposal

If you store GPU resources (Effects, RenderTargets, etc) in the cache, prefer GetOrCreate so they’re created once per game.

val init: ctx: 'a -> struct ('b * 'c)
val ctx: 'a
val player: obj
val font: obj
val shader: obj
val outlineFx: obj
Multiple items
union case SkillId.SkillId: string -> SkillId

--------------------
type SkillId = | SkillId of string
type SkillId = | SkillId of string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
type Skill = { Id: SkillId Name: string CooldownSeconds: float32 }
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

--------------------
type float32 = System.Single

--------------------
type float32<'Measure> = float32
val skillDecoder: json: 'a -> 'b
val json: 'a
val id: x: 'T -> 'T
Multiple items
type SkillStore = new: assets: obj -> SkillStore member IndexByIdCached: path: string * ctx: 'a -> Map<SkillId,Skill> member LoadAll: path: string -> Skill list member LoadAllCached: path: string * ctx: 'b -> Skill list

--------------------
new: assets: obj -> SkillStore
val assets: obj
val path: string
type 'T list = List<'T>
module SkillDecoders from assets
val ctx: 'b
Multiple items
module Map from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...

--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
val map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
val ofList: elements: ('Key * 'T) list -> Map<'Key,'T> (requires comparison)
val store: SkillStore
val skillsById: Map<SkillId,Skill>
member SkillStore.IndexByIdCached: path: string * ctx: 'a -> Map<SkillId,Skill>
 A typed cache for an indexed view.
Multiple items
type SkillStoreDb = new: conn: IDbConnection -> SkillStoreDb member GetById: id: SkillId -> Skill option

--------------------
new: conn: System.Data.IDbConnection -> SkillStoreDb
val conn: System.Data.IDbConnection
namespace System
namespace System.Data
type IDbConnection = inherit IDisposable override BeginTransaction: unit -> IDbTransaction + 1 overload override ChangeDatabase: databaseName: string -> unit override Close: unit -> unit override CreateCommand: unit -> IDbCommand override Open: unit -> unit member ConnectionString: string member ConnectionTimeout: int member Database: string member State: ConnectionState
<summary>Represents an open connection to a data source, and is implemented by .NET data providers that access relational databases.</summary>
val id: SkillId
type 'T option = Option<'T>
union case Option.None: Option<'T>

Type something to start searching.