Skip to content

kikyujin/YAMAMVA

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

YamAMVA (山姥)

YAML Accounted Multi adV. Architect

A pure scenario dispatcher written in Rust. Parse YAML, drive your game — YamAMVA handles state, branching, and blocking menus while knowing nothing about rendering, audio, or AI.

while (1) {
    int id = yamamva_exec(h, &args);
    if (id == YAMAMVA_END) break;

    switch (id) {
    case CMD_BG:   show_bg(&args);    break;
    case CMD_TEXT:  show_text(&args);  break;
    case CMD_MENU:  show_menu(&args);  break;
    }
}

Windows message loop meets visual novels. YamAMVA is GetMessage; your game is DispatchMessage.


Features

  • Engine-agnostic — Unity (C# P/Invoke), Python (ctypes), C/C++, WASM
  • Pull-based execution — your game calls exec(), not the other way around
  • Registration model — define your own node types; YamAMVA dispatches by command ID
  • World / Scene scope — split scenarios across multiple YAML files with file:scene jump notation
  • Built-in flow controljump / do / when / incase / end
  • Expression evaluatorscore >= 80 and not accused, count + 1
  • Conditional elements — menu items filtered by when before reaching your game
  • Save / restore — snapshot to JSON, resume from any point
  • Zero rendering opinion — same YAML drives 3D, 2D, retro pixel, or web scroll
  • Minimal dependencies — only serde + serde_yaml + serde_json

Part of MindFox OSS

MindFox OSS
├── MxBS        — NPC memory engine             (Rust, standalone)
├── MxMindFox   — Mood & decision system         (Rust, MxBS workspace)
└── YamAMVA     — YAML scenario dispatcher       (Rust, standalone) ← this

Three crates, all Rust, all with C bindings, zero interdependence. Combine as needed.


Quick Start

Build

cargo build --release
# Output: target/release/libyamamva.so (Linux)
#         target/release/libyamamva.dylib (macOS)
#         target/release/yamamva.dll (Windows)

Write a Scenario

id: hello
title: "Hello YamAMVA"
version: "1.0"
entry: scene_start

state:
  greeted: false

scenes:
  scene_start:
    - speak:
        character: elmar
        text: "Hello! Pick a place."
        emotion: joy

    - menu:
        style: vertical
        elements:
          - { key: park, label: "Go to the park" }
          - { key: home, label: "Stay home" }

    - incase:
        - when: "$result == 'park'"
          next: scene_park
        - next: scene_home

  scene_park:
    - speak:
        character: elmar
        text: "The park is nice today!"
    - do:
        greeted: true
    - end: true

  scene_home:
    - speak:
        character: elmar
        text: "Home sweet home."
    - end: true

Integrate (C)

#include <stdio.h>

#define CMD_SPEAK 1
#define CMD_MENU  2

int main() {
    const char* yaml = load_file("hello.yaml");
    YamamvaHandle* h = yamamva_load(yaml, strlen(yaml));

    yamamva_register(h, "speak", CMD_SPEAK, YAMAMVA_PASS);
    yamamva_register(h, "menu",  CMD_MENU,  YAMAMVA_BLOCKING);

    YamamvaArgs args = {0};
    while (1) {
        int id = yamamva_exec(h, &args);
        if (id == YAMAMVA_END) break;

        switch (id) {
        case CMD_SPEAK:
            printf("Speech: %s\n", args.node_json);
            break;
        case CMD_MENU:
            printf("Menu with %d choices\n", args.element_count);
            args.result = "park";  // player's choice
            break;
        }
    }

    yamamva_free(h);
    return 0;
}

Integrate (Unity C# P/Invoke)

byte[] yaml = File.ReadAllBytes("scenario.yaml");
IntPtr h = YamamvaBridge.yamamva_load(yaml, (uint)yaml.Length);

YamamvaBridge.yamamva_register(h, "speak", CMD_SPEAK, YAMAMVA_PASS);
YamamvaBridge.yamamva_register(h, "move",  CMD_MOVE,  YAMAMVA_PASS);
YamamvaBridge.yamamva_register(h, "menu",  CMD_MENU,  YAMAMVA_BLOCKING);

// exec loop in a coroutine — see examples/unity/

Integrate (Python — bridge class)

from yamamva_bridge import YamamvaBridge

CMD_SPEAK = 1
CMD_MENU  = 2

yamva = YamamvaBridge(yaml_str)                         # load from string
yamva.register("speak", CMD_SPEAK)                      # PASS
yamva.register("menu",  CMD_MENU, blocking=True)        # BLOCKING

while True:
    cmd, info = yamva.exec()
    if cmd == yamva.END:
        break
    if cmd == CMD_SPEAK:
        print(info["node_json"]["text"])
    elif cmd == CMD_MENU:
        yamva.set_result("park")                        # player's choice

yamva.close()

See python/yamamva_bridge.py for the full wrapper.

Integrate (Python — raw ctypes)

import ctypes

lib = ctypes.CDLL("./libyamamva.so")
h = lib.yamamva_load(yaml_bytes, len(yaml_bytes))

lib.yamamva_register(h, b"speak", 1, 0)  # PASS
lib.yamamva_register(h, b"menu",  2, 1)  # BLOCKING

# exec loop — see test_yamamva_ffi.py

YAML Syntax

Single-file Scenario

id: my_scenario          # required
title: "My Scenario"     # required
version: "1.0"
entry: scene_first       # required — first scene to execute

state: {}                # initial state variables

characters: {}           # metadata (YamAMVA does NOT resolve names)
backgrounds: {}          # metadata
bgm: {}                  # metadata

scenes:                  # required
  scene_first:
    - ...nodes...

Multi-file World (v1.0)

Split large scenarios across files using world.yaml + scenes/ directory:

my_game/
├── world.yaml           ← global state, metadata, entry point
└── scenes/
    ├── intro.yaml       ← scene_intro
    ├── hub.yaml         ← scene_hub, scene_shop
    └── endings.yaml     ← scene_win, scene_lose
# world.yaml
id: my_game
title: "My Game"
entry: intro:scene_intro     # file:scene notation
scene_path: scenes/
state:
  gold: 100
characters:
  hero: { name: "Hero" }
# scenes/hub.yaml
scene_hub:
  - text: "Where to?"
  - menu:
      elements:
        - { key: shop, label: "Shop" }
        - { key: end, label: "Leave" }
  - incase:
      - when: "$result == 'shop'"
        next: scene_shop              # same file — no prefix needed
      - next: endings:scene_win       # cross-file — file:scene required
scene_shop:
  - text: "Welcome to the shop."
  - jump:
      - next: scene_hub

Load with yamamva_load_world() (C API) or parse_world() (Rust). Single-file scenarios loaded via yamamva_load() remain fully compatible.

Node Types

Any YAML key becomes a node type. Register it and YamAMVA dispatches it. Unregistered nodes are silently skipped.

# Simple pass-through nodes
- bg: lobby
- text: "You entered the room."
- speak:
    character: elmar
    text: "Welcome!"
    emotion: joy

# Blocking node (game must write args.result)
- menu:
    style: vertical
    elements:
      - { key: sword, label: "Buy Sword (500G)", price: 500, when: "gold >= 500" }
      - { key: leave, label: "Leave" }

# Node-level when (skipped if false)
- speak:
    character: elmar
    text: "You already bought a sword!"
    when: "has_sword"

Built-in Nodes (5)

These are handled internally. Your game never sees them.

Node Purpose Example
do Update state - do: { score: "score + 10" }
jump Branch to scene - jump: [{ when: "score >= 80", next: good_end }, { next: bad_end }]
incase Branch on $result - incase: [{ when: "$result == 'yes'", next: scene_a }, { next: scene_b }]
when Conditional skip (attached to any node)
end End scenario - end: true

Expression Engine

Category Operators
Comparison == != < > <= >=
Logic and or not
Arithmetic + - * /
Types true false 42 3.14 "string"
Special $result (last BLOCKING return value)

Whitelist-only. No arbitrary code execution.


C API Reference

Lifecycle

YamamvaHandle* yamamva_load(const char* yaml, uint32_t len);
YamamvaHandle* yamamva_load_world(const char* world_yaml_path);
void           yamamva_free(YamamvaHandle* h);

Registration

void yamamva_register(YamamvaHandle* h, const char* node_type,
                      int32_t command_id, int32_t flags);
// flags: YAMAMVA_PASS (0) or YAMAMVA_BLOCKING (1)

Execution

int32_t yamamva_exec(YamamvaHandle* h, YamamvaArgs* args);
// Returns: command_id (>= 1) or YAMAMVA_END (-1)

State

const char* yamamva_get_state(const YamamvaHandle* h, const char* key);
void        yamamva_set_state(YamamvaHandle* h, const char* key,
                              const char* value_json);

Metadata

const char* yamamva_meta(const YamamvaHandle* h, const char* section);
// section: "characters", "backgrounds", "bgm", "format", "state"

Save / Restore

const char* yamamva_save(const YamamvaHandle* h);
YamamvaHandle* yamamva_restore(const char* yaml, uint32_t len,
                                const char* save_json);

Memory

void yamamva_free_string(const char* s);
// Free strings returned by yamamva_get_state, yamamva_meta, yamamva_save

Structures

typedef struct {
    const char* node_type;
    const char* node_json;       // full node as JSON
    uint32_t    element_count;
    const YamamvaElement* elements;
    const char* result;          // game writes this (BLOCKING only)
} YamamvaArgs;

typedef struct {
    const char* key;
    const char* label;
    const char* extra_json;      // all fields except key/label/when
} YamamvaElement;

Constants

#define YAMAMVA_END      (-1)
#define YAMAMVA_PASS     (0)
#define YAMAMVA_BLOCKING (1)

Total: 11 functions, 3 constants, 2 structs.


Design Philosophy

YamAMVA knows flow control and nothing else:

YamAMVA knows YamAMVA does NOT know
YAML structure How to draw backgrounds
State variables Character names → sprites/models
Conditional branching Menu UI design
when element filtering Audio playback
Save/restore snapshots LLM calls
Command dispatch Network, database, physics

Same YAML, different games:

Platform speaker: elmar resolves to
Unity (3D) VRM model with lip sync
Web (scroll) PNG sprite overlay
Retro PC 16-color pixel portrait

Examples

See the examples/ directory:

  • ellmar_tour.yaml — Room tour with move/speak/menu (used in ELLMAR-Unity integration test)
  • oyatsu_adv.yaml — Mystery ADV with hearing menu, conditional branching, state tracking

Python Bridge

python/yamamva_bridge.py — full ctypes wrapper class.

from yamamva_bridge import YamamvaBridge

yamva = YamamvaBridge(yaml_str)
yamva.register("speak", 1)
yamva.register("menu", 2, blocking=True)

cmd, info = yamva.exec()
# info = {"node_type": str, "node_json": dict, "elements": [{"key", "label", "extra"}]}

yamva.set_result("choice_key")          # answer BLOCKING node
val = yamva.get_state("score")          # read state variable
yamva.set_state("score", 100)           # write state variable
save_json = yamva.save()                # serialize for save file

# Restore from save
yamva2 = YamamvaBridge.restore(yaml_str, save_json)

All 11 C API functions are wrapped. Requires libyamamva.dylib/.so (run cargo build --release).


Tests

cargo test
# 36 tests (21 unit + 15 world/scope integration), 0 warnings

# FFI tests (requires Python 3)
python test_yamamva_ffi.py
# 5 tests

日本語 / Japanese

YamAMVA(山姥)は YAML シナリオ・ディスパッチャです。

YAML で書かれたシナリオの進行制御(ステート管理・条件分岐)だけを行い、描画・音声・LLM・メニューUI の「意味」は一切知りません。ゲーム側が登録したコマンドIDに対して引数を返すだけの、純粋なステートマシンです。

Windows のメッセージループと同じ構造 —— 山姥が GetMessage、ゲーム側が DispatchMessage

MindFox OSS ファミリー

クレート 役割
MxBS NPC記憶エンジン
MxMindFox 感情 & 意思決定
YamAMVA YAML シナリオディスパッチャ

3つとも Rust、3つとも C bindings 付き、相互依存なし。


License

MIT License — see LICENSE

Authors

MULTITAPPS INC. — Mahito KIDA + エルマー🦊

About

YAML scenario dispatcher for games — engine-agnostic, pull-based, with C bindings. YamAMVA is GetMessage; your game is DispatchMessage.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors