Skip to content

Latest commit

 

History

History
895 lines (686 loc) · 29.8 KB

File metadata and controls

895 lines (686 loc) · 29.8 KB

Usage Guide

This guide covers every feature of the entity-generator library in detail. For a quick overview, see the README. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html.

[TOC]

Defining Components

#include <dasmig/entitygen.hpp>

// A component that generates a random character class.
class character_class : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"class"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        static const std::vector<std::wstring> classes{
            L"Warrior", L"Mage", L"Rogue", L"Healer"};
        return ctx.random().get(classes);
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

// A component that generates a random age.
class age : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"age"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        return ctx.random().get(18, 65);
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

default_to_string() handles std::wstring, int, double, float, long, and bool. All other types return [?] — override to_string() for custom types.

Registering and Generating

using eg = dasmig::eg;

// Register components with fluent chaining.
eg::instance()
    .add(std::make_unique<character_class>())
    .add(std::make_unique<age>());

// Generate an entity with all registered components.
auto entity = eg::instance().generate();

// Generate an entity with only specific components.
auto partial = eg::instance().generate({L"class"});

// Output to stream.
std::wcout << eg::instance().generate() << std::endl;

// Generate with a seed for reproducible results.
auto seeded = eg::instance().generate(42);

// Seed the generator for a deterministic sequence.
eg::instance().seed(123);
auto first  = eg::instance().generate();
auto second = eg::instance().generate();
eg::instance().unseed();

Behavior notes:

  • add() with a key that already exists replaces the component in place, preserving registration order. Any existing weight override for that key persists.
  • remove() on a non-existent key is a no-op (does not throw).
  • remove_group() on a non-existent group is a no-op.

Typed Retrieval

auto entity = eg::instance().generate();

// Access values with their original types.
std::wstring char_class = entity.get<std::wstring>(L"class");
int char_age = entity.get<int>(L"age");

// Type-erased access (returns const std::any&).
const auto& raw = entity.get_any(L"class");

// Check if a component exists.
if (entity.has(L"name")) { /* ... */ }

// Iterate over all keys in generation order.
for (const auto& key : entity.keys()) { /* ... */ }

Component Dependencies

Components receive a generation_context that provides access to values generated by earlier components. Registration order determines generation order.

// A greeting component that depends on a previously generated name.
class greeting : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"greeting"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        // Access a value generated by an earlier component.
        auto name = ctx.get<std::wstring>(L"name");
        return L"Hello, " + name + L"!";
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

// Register name before greeting so the dependency is available.
eg::instance()
    .add(std::make_unique<character_name>())
    .add(std::make_unique<greeting>());

The context also exposes ctx.has(key) to check whether a dependency was generated (useful when generating with a subset of components).

Composing with Other Generators

Components can wrap name-generator and nickname-generator to bring in name and nickname generation:

#include <dasmig/entitygen.hpp>
#include <dasmig/namegen.hpp>
#include <dasmig/nicknamegen.hpp>

class full_name : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"name"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        return static_cast<std::wstring>(
            dasmig::ng::instance().get_name().append_surname());
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

class player_nickname : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"nickname"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        return static_cast<std::wstring>(
            dasmig::nng::instance().get_nickname());
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

Custom Types

Components can produce any type. Override to_string() on your component to enable stream output:

struct vec2 { float x, y; };

class position : public dasmig::component
{
  public:
    [[nodiscard]] std::wstring key() const override { return L"position"; }
    [[nodiscard]] std::any generate(
        const dasmig::generation_context& ctx) const override
    {
        return vec2{
            ctx.random().get(-100.f, 100.f),
            ctx.random().get(-100.f, 100.f)};
    }
    [[nodiscard]] std::wstring to_string(const std::any& value) const override
    {
        auto pos = std::any_cast<vec2>(value);
        return L"(" + std::to_wstring(pos.x) + L", " + std::to_wstring(pos.y) + L")";
    }
};

Seed Signatures

Every generated entity and component stores the random seed that produced it. This enables replay, debugging, and logging.

auto entity = eg::instance().generate();

// Retrieve the entity-level seed. This single value can reproduce
// all component seeds and thus the entire entity.
std::uint64_t entity_seed = entity.seed();

// Retrieve the seed used for a specific component.
std::uint64_t age_seed = entity.seed(L"age");

Component seeds can be used to replay individual component generation:

// Replay a component's random values using its captured seed.
effolkronium::random_local rng;
rng.seed(static_cast<std::mt19937::result_type>(entity.seed(L"age")));
int replayed_age = rng.get(18, 65); // Same value as entity.get<int>(L"age")

The seed hierarchy is fully deterministic:

generation seed (per-call or generator-level)
  └─ entity seed          ← entity.seed()
       └─ local engine
            ├─ component A seed   ← entity.seed(L"A")
            ├─ component B seed   ← entity.seed(L"B")
            └─ ...

Batch Generation

Generate multiple entities in a single call:

// Generate 10 entities with all registered components.
auto batch = eg::instance().generate_batch(10);

for (const auto& e : batch)
{
    std::wcout << e << L'\n';
}

// Seeded batch for deterministic results.
auto seeded_batch = eg::instance().generate_batch(10, 42);

Component Groups

Define named groups of component keys for convenient selective generation:

// Register components.
eg::instance()
    .add(std::make_unique<character_name>())
    .add(std::make_unique<character_class>())
    .add(std::make_unique<age>())
    .add(std::make_unique<level>())
    .add(std::make_unique<character_stats>());

// Define groups.
eg::instance()
    .add_group(L"identity", {L"name", L"class"})
    .add_group(L"combat", {L"class", L"level", L"stats"});

// Generate using a group.
auto fighter = eg::instance().generate_group(L"combat");

// Seeded group generation.
auto seeded = eg::instance().generate_group(L"combat", 42);

// Check and remove groups.
if (eg::instance().has_group(L"identity")) { /* ... */ }
eg::instance().remove_group(L"identity");

Thread Safety

The eg class supports independent instances. Create one per thread for lock-free concurrent generation:

// Each thread gets its own generator — no locks needed.
std::vector<std::jthread> threads;
for (int i = 0; i < 4; ++i)
{
    threads.emplace_back([&](int id) {
        dasmig::eg gen;
        gen.add(std::make_unique<name>(/**/));
        gen.seed(id);
        auto e = gen.generate();
    }, i);
}

eg is move-constructible and move-assignable, so instances can be transferred between scopes. The instance() singleton is still available for single-threaded convenience.

Component Weights

Components can declare an inclusion weight via the weight() virtual method (default 1.0). Values range from 0.0 (never included) to 1.0 (always included). The generator rolls against the weight during generation; components that fail the roll are skipped.

class rare_trait : public dasmig::component
{
  public:
    std::wstring key() const override { return L"rare_trait"; }
    double weight() const override { return 0.2; } // 20% chance

    std::any generate(const dasmig::generation_context& ctx) const override
    {
        return ctx.random().get<std::wstring>({L"scar", L"tattoo", L"birthmark"});
    }

    std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

Weights can also be overridden at registration time or updated later, taking precedence over the component's own weight() method:

// Override weight at registration.
eg::instance().add(std::make_unique<rare_trait>(), 0.5);

// Update weight after registration.
eg::instance().weight(L"rare_trait", 0.8);

Note: When a weighted component is skipped, dependent components that call ctx.get<T>() for it will throw. Use ctx.has() to guard dependency access in weight-sensitive components.

Entity Serialization

Convert an entity to a formatted string with to_string(). Each component is rendered as "key: display" separated by two spaces. The output matches operator<<.

auto entity = eg::instance().generate();

// Get the formatted string.
std::wstring text = entity.to_string();
// e.g. "name: Alice  class: Mage  age: 34"

// Equivalent to streaming:
std::wcout << entity << L'\n';

Structured Serialization

to_map() returns component values as a std::map<std::wstring, std::wstring> mapping keys to their display strings. This is convenient for integration with JSON, XML, or any key–value serialization format.

auto entity = eg::instance().generate();
auto m = entity.to_map();
// m["name"] = "Alice", m["class"] = "Mage", m["age"] = "34"

// Iterate all entries.
for (const auto& [key, value] : m)
{
    std::wcout << key << L" = " << value << L'\n';
}

Custom types work seamlessly — the map uses each component's to_string() output:

auto m = entity.to_map();
// m["pos"] = "(3,7)" — uses custom_type_component::to_string()

Concurrent Batch Generation

generate_batch_async() generates entities in parallel using std::async. Entity seeds are pre-derived sequentially from the engine, then each entity is generated in its own async task. Results are returned in seed order.

// Generate 100 entities concurrently.
auto batch = eg::instance().generate_batch_async(100);

// Seeded: deterministic regardless of thread scheduling.
auto batch1 = eg::instance().generate_batch_async(50, 42);
auto batch2 = eg::instance().generate_batch_async(50, 42);
// batch1[i] == batch2[i] for all i

Entity-level validation applies to each concurrent task independently. If an observer is set, the caller is responsible for its thread safety (hooks fire from multiple threads concurrently).

Validation Hooks

Validation operates at two levels, both opt-in with zero-cost defaults.

Component-level: Override validate() on your component. If it returns false, the generator re-rolls with a new seed up to max_retries times, then throws std::runtime_error.

class even_age : public dasmig::component
{
  public:
    std::wstring key() const override { return L"age"; }
    std::any generate(const dasmig::generation_context& ctx) const override
    {
        return ctx.random().get(18, 65);
    }
    bool validate(const std::any& value) const override
    {
        return std::any_cast<int>(value) % 2 == 0;
    }
    std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

Entity-level: Set a callback on the generator. If it returns false, the entire entity is re-generated up to max_retries additional times.

eg::instance().set_validator([](const dasmig::entity& e) {
    return e.get<int>(L"age") > 30;
});

auto e = eg::instance().generate(); // age is guaranteed > 30

eg::instance().clear_validator(); // remove the validator

Configure the retry limit (default 10) for both levels:

eg::instance().max_retries(20);

Event Hooks

Attach one or more generation_observer instances to any generator to receive lifecycle callbacks. Override only the hooks you need; all default to no-ops.

class log_observer : public dasmig::generation_observer
{
  public:
    void on_before_generate() override
    {
        std::wcout << L"generating entity...\n";
    }
    void on_after_component(const std::wstring& key,
                            const std::any& /*value*/) override
    {
        std::wcout << L"produced " << key << L'\n';
    }
};

auto obs = std::make_shared<log_observer>();
eg::instance().add_observer(obs);
eg::instance().generate(); // prints hook messages
eg::instance().remove_observer(obs);

// Or remove all observers at once.
eg::instance().clear_observers();

Multiple observers fire in registration order. The same observer can be added more than once.

Thread safety: Observers are not thread-safe by default. When using generate_batch_async, the caller is responsible for ensuring observer implementations are safe for concurrent invocation (e.g., using atomics or mutexes).

The full set of hooks (6 before/after pairs + 3 single-fire hooks, 15 methods):

Event Hook(s)
Entity generation on_before_generate() / on_after_generate(entity)
Component generation on_before_component(key) / on_after_component(key, value)
Component skip on_skip(key)
Component retry on_before_retry(key, attempt) / on_after_retry(key, attempt, value)
Component fail on_component_fail(key)
Entity retry on_before_entity_retry(attempt) / on_after_entity_retry(attempt)
Entity fail on_entity_fail()
Registration on_before_add(key) / on_after_add(key)
Removal on_before_remove(key) / on_after_remove(key)

For per-component filtering, check the key parameter inside your hook override.

Conditional Components

Components can override should_generate(ctx) to conditionally skip generation based on the current context. Unlike weights (probabilistic), this is logic-driven. The default returns true.

class warrior_title : public dasmig::component
{
  public:
    std::wstring key() const override { return L"title"; }
    std::any generate(const dasmig::generation_context& ctx) const override
    {
        static const std::vector<std::wstring> titles{
            L"Warlord", L"Champion", L"Berserker"};
        return ctx.random().get(titles);
    }
    bool should_generate(const dasmig::generation_context& ctx) const override
    {
        return ctx.has(L"class")
            && ctx.get<std::wstring>(L"class") == L"Warrior";
    }
    std::wstring to_string(const std::any& value) const override
    {
        return default_to_string(value);
    }
};

When should_generate returns false, the component is skipped entirely — no value is produced and entity.has(key) returns false. The skip fires the same observer hooks as a weight skip (on_before_skip / on_after_skip).

Note: should_generate is checked after the weight roll. If a component is excluded by weight, should_generate is never called.

Generic Components

Five reusable class templates for the most common component patterns. All live in the dasmig:: namespace and accept an optional custom formatter as a trailing template parameter.

constant_component<T>

Always returns the same value.

gen.add(std::make_unique<dasmig::constant_component<int>>(L"version", 1));
gen.add(std::make_unique<dasmig::constant_component<std::wstring>>(
    L"label", L"npc"));

choice_component<T>

Picks uniformly at random from a list.

gen.add(std::make_unique<dasmig::choice_component<std::wstring>>(
    L"class", std::vector<std::wstring>{L"Warrior", L"Mage", L"Rogue"}));

range_component<T>

Uniform random in a numeric range [lo, hi]. Restricted to arithmetic types.

gen.add(std::make_unique<dasmig::range_component<int>>(L"age", 18, 65));
gen.add(std::make_unique<dasmig::range_component<double>>(L"luck", 0.0, 1.0));

callback_component

Wraps a callable that takes generation_context& and returns a value. Avoids subclassing for one-off or context-dependent components. Use std::function to erase the callable type for make_unique.

using cb_wstr = dasmig::callback_component<std::wstring,
    std::function<std::wstring(const dasmig::generation_context&)>>;
gen.add(std::make_unique<cb_wstr>(
    L"greeting", [](const dasmig::generation_context& ctx) -> std::wstring {
        return L"Hello, " + ctx.get<std::wstring>(L"name") + L"!";
    }));

A CTAD deduction guide is provided for stack-allocated usage where make_unique is not needed.

weighted_choice_component<T>

Picks from a list of values using per-option weights. Weights are relative (they don't need to sum to 1.0). A weight of 0 means the option is never selected.

using rarity_t = dasmig::weighted_choice_component<std::wstring>;
gen.add(std::make_unique<rarity_t>(
    L"rarity", std::vector<rarity_t::option>{
        {L"Common", 60.0}, {L"Uncommon", 25.0},
        {L"Rare", 10.0}, {L"Legendary", 5.0}}));

Custom formatters

All generic components use default_to_string by default. Pass a custom formatter as an extra template parameter and constructor argument for types it doesn't handle:

auto fmt = [](int v) { return L"#" + std::to_wstring(v); };
gen.add(std::make_unique<dasmig::constant_component<int, decltype(fmt)>>(
    L"id", 5, fmt));
// to_map()["id"] == "#5"

Entity Introspection

auto entity = eg::instance().generate();

// Number of component values.
std::size_t n = entity.size();

// Check if empty.
if (entity.empty()) { /* ... */ }

// All keys in generation order.
auto keys = entity.keys();

// Type-erased access.
const std::any& val = entity.get_any(L"class");

// Seed of the entity and individual components.
auto entity_seed = entity.seed();
auto comp_seed = entity.seed(L"class");

// Display string map.
auto m = entity.to_map(); // map<wstring, wstring>

Generator Introspection

dasmig::eg gen;
gen.add(std::make_unique<age>())
   .add(std::make_unique<character_class>());

// Number of registered components.
std::size_t n = gen.size(); // 2

// All registered keys in registration order.
auto keys = gen.component_keys(); // {"age", "class"}

// Check if a specific component is registered.
bool has_age = gen.has(L"age"); // true

// Remove everything (components, weight overrides, groups).
gen.clear();

Extensions

Optional headers in dasmig/ext/ provide ready-made functionality built on the observer interface.

stats_observer

#include <dasmig/ext/stats_observer.hpp>

Tracks comprehensive generation statistics: counts, timing, per-component breakdowns, and value distributions. Attach it like any other observer; read the fields after generation.

auto stats = std::make_shared<dasmig::ext::stats_observer>();
gen.add_observer(stats);

gen.generate_batch(100);

std::wcout << L"Entities:   " << stats->entities_generated  << L'\n'
           << L"Components: " << stats->components_generated << L'\n'
           << L"Skipped:    " << stats->components_skipped   << L'\n'
           << L"Failures:   " << stats->component_failures   << L'\n';

// Per-key counts.
for (const auto& [key, n] : stats->component_counts)
    std::wcout << key << L": " << n << L" generated\n";

// Per-key retry counts.
for (const auto& [key, n] : stats->component_retries)
    std::wcout << key << L": " << n << L" retries\n";

// Timing.
using ms = std::chrono::milliseconds;
std::wcout << L"Avg entity: "
           << std::chrono::duration_cast<ms>(stats->avg_entity_time()).count()
           << L" ms\n";
for (const auto& [key, _] : stats->component_counts)
    std::wcout << key << L" avg: "
               << std::chrono::duration_cast<ms>(
                      stats->avg_component_time(key)).count()
               << L" ms\n";

// Value distribution.
for (const auto& [key, dist] : stats->value_distribution)
    for (const auto& [val, count] : dist)
        std::wcout << key << L"=" << val << L": " << count << L'\n';

stats->reset(); // zeroes all counters

// Generate a full formatted report.
std::wcout << stats->report();

// Or use the stream operator (equivalent).
std::wcout << *stats;

Entity counters:

Field Description
entities_generated Successful entities produced
entity_retries Entity-level validation retries
entity_failures Entity validation exhausted (threw)

Component counters:

Field Description
components_generated Total component values produced
components_skipped Weight or conditional skips
component_failures Component validation exhausted (threw)

Per-component-key maps (all std::map<std::wstring, …>):

Field Value type Description
component_counts size_t Times each key was generated
component_skip_counts size_t Times each key was skipped
component_failure_counts size_t Times each key failed
component_retries size_t Total retries per key
component_times duration Cumulative wall-clock per key
component_min_times duration Fastest generation per key
component_max_times duration Slowest generation per key

Entity timing:

Field Description
total_generation_time Sum of wall-clock across all entities
min_entity_time Fastest entity generation
max_entity_time Slowest entity generation

Components-per-entity:

Field Description
min_components_per_entity Fewest components in a single entity
max_components_per_entity Most components in a single entity
total_components_in_entities Sum for computing averages

Value distribution:

value_distribution is a map<wstring, map<wstring, size_t>> — for each component key, counts how many times each display value appeared. Common std::any types (wstring, int, double, float, long, bool) are stringified automatically; others record as <other>.

Computed helpers:

Method Returns
avg_entity_time() total_generation_time / entities_generated
avg_component_time(key) per-key average generation time
avg_components_per_entity() double average
component_retry_rate() total retries / components generated
entity_retry_rate() entity retries / entities generated

ECS Integration: EnTT

dasmig/ext/entt_adapter.hpp bridges entity-generator with EnTT. It maps generator component keys to typed EnTT components via user-defined callables.

Requires: EnTT header (<entt/entt.hpp>) available on the include path.

#include <dasmig/entitygen.hpp>
#include <dasmig/ext/entt_adapter.hpp>

// ECS component types
struct Name { std::wstring value; };
struct Position { float x, y; };

// Set up generator
dasmig::eg gen;
gen.add(std::make_unique<dasmig::constant_component<std::wstring>>(L"name", L"Alice"))
   .add(std::make_unique<dasmig::range_component<float>>(L"x", 0.f, 100.f))
   .add(std::make_unique<dasmig::range_component<float>>(L"y", 0.f, 100.f));

// Set up adapter
entt::registry registry;
dasmig::ext::entt_adapter adapter(registry);

adapter.map<Name>(L"name", [](const dasmig::entity& e) {
    return Name{e.get<std::wstring>(L"name")};
});

// Multi-key → single component: triggered by key "x", reads both "x" and "y"
adapter.map<Position>(L"x", [](const dasmig::entity& e) {
    return Position{e.get<float>(L"x"), e.get<float>(L"y")};
});

// Spawn
auto entt_entity = adapter.spawn(gen.generate());
registry.get<Name>(entt_entity);     // Name{"Alice"}
registry.get<Position>(entt_entity); // Position{...}

// Batch spawn
auto batch = gen.generate_batch(100);
auto entities = adapter.spawn_batch(batch); // 100 EnTT entities

Direct mapping (no transform, when generator value type == ECS type):

gen.add(std::make_unique<dasmig::range_component<int>>(L"hp", 50, 200));
adapter.map<int>(L"hp"); // any_cast<int> directly emplaced

Spawn into existing entity:

auto prefab = registry.create();
registry.emplace<some_tag>(prefab);
adapter.spawn_into(prefab, gen.generate()); // adds mapped components

Skipped components (weight = 0, conditional = false) are silently ignored — no EnTT component is emplaced for that key.

ECS Integration: Flecs

dasmig/ext/flecs_adapter.hpp bridges entity-generator with Flecs. Same mapping pattern as the EnTT adapter.

Requires: Flecs header (<flecs.h>) and compiled flecs.c linked.

#include <dasmig/entitygen.hpp>
#include <dasmig/ext/flecs_adapter.hpp>

struct Name { std::wstring value; };
struct Position { float x, y; };

dasmig::eg gen;
gen.add(std::make_unique<dasmig::constant_component<std::wstring>>(L"name", L"Alice"))
   .add(std::make_unique<dasmig::range_component<float>>(L"x", 0.f, 100.f))
   .add(std::make_unique<dasmig::range_component<float>>(L"y", 0.f, 100.f));

flecs::world world;
dasmig::ext::flecs_adapter adapter(world);

adapter.map<Name>(L"name", [](const dasmig::entity& e) {
    return Name{e.get<std::wstring>(L"name")};
});

adapter.map<Position>(L"x", [](const dasmig::entity& e) {
    return Position{e.get<float>(L"x"), e.get<float>(L"y")};
});

auto flecs_entity = adapter.spawn(gen.generate());
flecs_entity.get<Name>();     // const Name&
flecs_entity.get<Position>(); // const Position&

// Batch spawn
auto entities = adapter.spawn_batch(gen.generate_batch(100));

// Spawn into existing entity
auto prefab = world.entity();
adapter.spawn_into(prefab, gen.generate());

API summary (identical for both adapters):

Method Description
map<T>(key, fn) Register a key→component mapping with transform
map<T>(key) Direct mapping (value type must match T)
spawn(entity) Create ECS entity with mapped components
spawn_into(target, entity) Apply mapped components to existing entity
spawn_batch(vector) Spawn multiple entities at once
clear_mappings() Remove all registered mappings

Error Reference

The library throws standard exceptions. No custom exception types are used.

Exception Thrown by Condition
std::out_of_range entity::get<T>(key), entity::get_any(key) Key not present in the entity
std::out_of_range entity::seed(key) Component key not found
std::out_of_range eg::weight(key, value) Component not registered
std::out_of_range eg::generate_group(name) Group not found
std::runtime_error eg::generate() (all overloads) Component validation exhausted after max_retries
std::runtime_error eg::generate() (all overloads) Entity validation exhausted after max_retries
std::invalid_argument choice_component constructor Empty choices vector
std::invalid_argument weighted_choice_component constructor Empty options vector or all weights are zero
std::bad_any_cast entity::get<T>(key), generation_context::get<T>(key) Type mismatch on the stored value