Header menu logo Mibo

Assets (loading + caching)

Mibo provides a simple, functional API for loading and caching game assets through the Mibo.Elmish.Assets module. It wraps MonoGame's ContentManager with automatic caching so you never load the same texture twice.

Enabling the service

Add it in your composition root:

Program.mkProgram init update
|> Program.withAssets

Loading Standard Assets

Once enabled, use these functions anywhere you have a GameContext:

let init (ctx: GameContext): struct(Model * Cmd<Msg>) =
  // Load and cache content pipeline assets
  let player = Assets.texture "sprites/player" ctx
  let font = Assets.font "fonts/ui" ctx
  let bgm = Assets.sound "audio/background" ctx
  let enemyModel = Assets.model "models/enemy" ctx
  let shader = Assets.effect "Effects/Grid" ctx
  
  { PlayerTex = player
    Font = font
    Bgm = bgm
    Enemy = enemyModel
    Shader = shader }, Cmd.none

All these functions cache results automatically:

Function

Returns

Description

Assets.texture path ctx

Texture2D

2D image asset

Assets.font path ctx

SpriteFont

Bitmap font

Assets.sound path ctx

SoundEffect

Audio effect

Assets.model path ctx

Model

3D model

Assets.effect path ctx

Effect

Shader/effect

Custom Assets

For assets not loaded through the content pipeline, use getOrCreate:

<<<<<<< Updated upstream
// Create a custom shader once, reuse forever
let outlineFx =
  Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx

// Create a render target
let rt =
  Assets.getOrCreate "MainRenderTarget" 
    (fun gd -> new RenderTarget2D(gd, 1920, 1080)) ctx

Why getOrCreate? It's idempotent - safe to call multiple times, creates only once.

For assets that might not exist yet, use get:

match Assets.get<MyConfig> "config" ctx with
| ValueSome cfg -> cfg
| ValueNone -> loadDefaultConfig()

To force creation (overwriting any existing), use create:

// This replaces any existing "playerData" entry
Assets.create "playerData" (fun _ -> loadPlayerFromDisk()) ctx

Loading Non-Pipeline Files

### JSON with JDeck

// Create once, cache forever - preferred approach let outlineFx = Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx

// Check if already cached without creating match Assets.get "OutlineEffect" ctx with | ValueSome fx -> printfn "Already loaded" | ValueNone -> printfn "Not cached yet"

// Force creation (overwrites existing - use with caution) let freshData = Assets.create "tempData" (fun _ -> calculateExpensive()) ctx

**Note:** `Assets.get` only retrieves custom assets created via `create` or `getOrCreate`. Content pipeline assets (textures, models, etc.) are loaded through `Assets.texture`, `Assets.model`, etc.

# JSON helpers
>>>>>>> Stashed changes

Mibo includes JSON helpers via [JDeck](https://github.com/AngelMunoz/JDeck):

```fsharp
// One-off load (no caching)
let config = Assets.fromJson "config.json" myDecoder

// Cached for game lifetime
let data = Assets.fromJsonCache "data/levels.json" levelDecoder ctx

For writing decoders, see the JDeck docs:

Custom File Loaders

Load any file type with custom logic:

// One-off load
let raw = Assets.fromCustom "data/save.dat" File.ReadAllBytes ctx

// Cached
let parsed = 
  Assets.fromCustomCache "data/items.csv" 
    (fun path -> parseCsvFile path) ctx

Building Data Stores

For game-specific datasets (skills, items, quests), wrap asset loading in a typed store:

type SkillId = SkillId of string

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

module SkillStore =
  let loadAll (path: string) : Skill list =
    // No caching - manage lifetime yourself
    Assets.fromJson path SkillDecoders.list
  
  let loadAllCached (path: string) (ctx: GameContext) : Skill list =
    // Cached for game lifetime
    Assets.fromJsonCache path SkillDecoders.list ctx
  
  let indexById (path: string) (ctx: GameContext) : Map<SkillId, Skill> =
    // Custom cached transformation
    Assets.getOrCreate ("skills/index/" + path)
      (fun _ ->
        let skills = Assets.fromJson path SkillDecoders.list
        skills |> List.map (fun s -> s.Id, s) |> Map.ofList)
      ctx

Usage:

let skillsById = SkillStore.indexById "Content/skills.json" ctx

Cache Behavior

Automatic caching applies to: - All standard assets (texture, font, sound, model, effect) - Anything loaded via getOrCreate or *Cache variants

Manual lifetime management: - Use Assets.fromJson or Assets.fromCustom for one-off loads - Build your own store with LRU/eviction if memory is a concern

Clearing caches:

// Get the underlying service (advanced)
let service = Assets.getService ctx

// Clear all custom caches (standard assets remain)
service.Clear()

// Dispose custom assets and clear everything
service.Dispose()

Advanced: Direct IAssets Access

For advanced scenarios, access the underlying IAssets service:

// Safe retrieval
match Assets.tryGetService ctx with
| ValueSome svc -> // use service
| ValueNone -> // service not registered

// Throws if not registered
let svc = Assets.getService ctx

IAssets provides the same operations if you need to pass it around as a value:

let loadStuff (assets: IAssets) =
  let tex = assets.Texture "sprite"
  let fx = assets.GetOrCreate "fx" (fun gd -> ...)
  ...

Prefer the Assets.* functions - they're more ergonomic and require less plumbing.

Performance Notes

For large games, consider:

  1. Chunked loading (per level/biome) with separate cache scopes
  2. Custom stores with LRU eviction for dynamic content
  3. Non-cached loads for one-time data
val init: ctx: 'a -> struct ('b * 'c)
val ctx: 'a
val player: obj
val font: obj
val bgm: obj
val enemyModel: obj
val shader: obj
val outlineFx: obj
val rt: obj
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val cfg: obj
union case ValueOption.ValueNone: ValueOption<'T>
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

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

--------------------
type float32<'Measure> = float32
type 'T list = List<'T>
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)

Type something to start searching.