Skip to content

Instantly share code, notes, and snippets.

@mikesmullin
Created May 27, 2025 01:07
Show Gist options
  • Save mikesmullin/02413bffb02759581e34ae32e7602fac to your computer and use it in GitHub Desktop.
Save mikesmullin/02413bffb02759581e34ae32e7602fac to your computer and use it in GitHub Desktop.
Mike's Fast ECS v1.0.0-alpha (static C impl)
// this is the ECS lib
// Generational Index
// assemble an entity id
inline GID GI_eid(bool pair, CID gen, CID arch, CID arch_idx) {
return //
(pair ? PAIR_BIT : 0U) | //
((u32)gen << GEN_SHIFT) | //
((u32)arch << ARCH_SHIFT) | //
((u32)arch_idx & ARCH_IDX_MASK);
}
inline CID GI_isPair(GID gid) {
return 0 != (gid & PAIR_BIT);
}
inline CID GI_gen(GID gid) {
return (gid & GEN_MASK) >> GEN_SHIFT;
}
inline CID GI_arch(GID gid) {
return (gid & ARCH_MASK) >> ARCH_SHIFT;
}
inline CID GI_arch_idx(GID gid) {
return (gid & ARCH_IDX_MASK);
}
#ifdef DEBUG_SLOW
DebugGenId db_gid(GID gid) {
return (DebugGenId){
.pair = GI_isPair(gid),
.gen = GI_gen(gid),
.arch = GI_arch(gid),
.arch_idx = GI_arch_idx(gid),
};
}
void GI_validate(GenIdx* gi, GID gid, CID slot, CID gen, CID arch, CID arch_idx) {
if (NULL != gi) {
ASSERT(NULL != gi->a);
ASSERT(gi->alive_ct <= ENT_MAX);
ASSERT(gi->dead_ct <= ENT_MAX);
ASSERT(gi->dead_head_idx <= ENT_MAX);
}
if (0 != gid) {
gen = GI_gen(gid);
arch = GI_arch(gid);
arch_idx = GI_arch_idx(gid);
}
ASSERT(slot <= ENT_MAX);
ASSERT(gen <= ENT_MAX);
ASSERT(arch <= ENT_MAX);
ASSERT(arch_idx <= ENT_MAX);
}
#else
DebugGenId db_gid(GID gid) {
}
void GI_validate(GenIdx* gi, GID gid, CID slot, CID gen, CID arch, CID arch_idx) {
}
#endif
// initialize the GenIdx struct
void GI_init() {
GenIdx* gi = &_G->world.gi;
// build a linked list of dead nodes from back to front
for (CID i = 0; i < ENT_MAX; i++) {
gi->a[i] = GI_eid(false, 0, 0, i + 1);
}
gi->dead_head_idx = 0;
gi->dead_ct = ENT_MAX;
gi->alive_ct = 0;
GI_validate(gi, 0, 0, 0, 0, 0);
}
// next available (recycling dead)
GID GI_get_next_id(CArch arch) {
GenIdx* gi = &_G->world.gi;
GI_validate(gi, 0, 0, 0, 0, 0);
CID slot = gi->dead_head_idx;
GID _gid = gi->a[slot];
CID gen = GI_gen(_gid); // keep generation
gi->dead_head_idx = GI_arch_idx(_gid); // next_dead_idx
gi->dead_ct--;
GID next_eid = GI_eid(false, gen, arch, slot);
gi->alive_ct++;
return next_eid;
}
// update/override a gen slot. (does not increment alive counter)
// could also be used to add arbitrary (out-of-order) new alive id to the index (ie. remote/server entities)
inline void GI_override(bool pair, CID slot, CID gen, CID arch, CID arch_idx) {
GenIdx* gi = &_G->world.gi;
gi->a[slot] = GI_eid(pair, gen, arch, arch_idx);
}
// check alive
bool GI_is_alive(GID gid) {
GenIdx* gi = &_G->world.gi;
GI_validate(gi, gid, 0, 0, 0, 0);
CID slot = GI_arch_idx(gid);
CID gen_a = GI_gen(gid);
GID b = gi->a[slot];
CID gen_b = GI_gen(b);
return gen_a == gen_b;
}
// apply death
void GI_set_dead(GID gid) {
GenIdx* gi = &_G->world.gi;
GI_validate(gi, gid, 0, 0, 0, 0);
// inc gen (only death can do this)
// point the idx to the old dead head
gi->a[GI_arch_idx(gid)] = GI_eid(false, GI_gen(gid) + 1, 0, gi->dead_head_idx);
// update head
gi->a[gi->dead_head_idx] = GI_eid(false, GI_gen(gid), 0, GI_arch_idx(gid));
gi->dead_head_idx = GI_arch_idx(gid);
gi->alive_ct--;
gi->dead_ct++;
}
// Paged Arrays
// allocate new page
void ECS__page_alloc(Paged** p, CID rec_cap, CID rec_sz) {
(*p) = (Paged*)Arena__Push(_G->arena, sizeof(Paged));
(*p)->records = Arena__Push /*Zero*/ (_G->arena, rec_sz * rec_cap);
(*p)->count = 0;
}
// append new record to existing page (or if page is full, create & link to new page)
void* ECS__page_record_add(List* list, CID rec_cap, CID rec_sz, void* record) {
Paged* p = NULL;
if (0 == list->len) {
ECS__page_alloc(&p, rec_cap, rec_sz);
List__append(_G->arena, list, p);
}
p = (Paged*)list->tail->data;
if (p->count >= rec_cap) {
ECS__page_alloc(&p, rec_cap, rec_sz);
List__append(_G->arena, list, p);
}
void* dst = p->records + (rec_sz * p->count++);
memcpy(dst, record, rec_sz); // copy
return dst;
}
// ECS
// allocate a new archetype
inline Archetype* ECS__arch_alloc(CArch arch, CID cmp_ct) {
Archetype* a = &_G->world.archs[arch];
a->type = arch;
a->cmp_ct = 0; // NOTE: assumes you will add the given number of components later
a->en_ct = 0;
List__init((List*)&a->entities);
a->cpools = Arena__Push(_G->arena, sizeof(ComponentPool) * cmp_ct);
a->dirty = OBS_NONE;
return a;
}
// add existing entity to existing archetype
CID ECS__arch_ent_add(GID entity) {
CArch arch = GI_arch(entity);
Archetype* a = &_G->world.archs[arch];
CID en_idx = a->en_ct++;
ECS__page_record_add((List*)&a->entities, CPOOL_PAGE_COUNT, sizeof(GID), &entity);
CID gen = GI_gen(entity);
CID arch_idx = GI_arch_idx(entity);
GI_override(false, arch_idx, gen, arch, en_idx);
a->dirty |= OBS_ADD;
return en_idx;
}
// mark existing entity dead, and set dirty flag on its archetype
void ECS__arch_ent_del(GID entity) {
CArch arch = GI_arch(entity);
Archetype* a = &_G->world.archs[arch];
GI_set_dead(entity);
a->dirty |= OBS_REMOVE;
}
// allocate new component pool
inline void ECS__arch_cpool_alloc(CArch arch, CCmp cmp, u16 sz) {
Archetype* a = &_G->world.archs[arch];
ComponentPool* c = &a->cpools[a->cmp_ct++];
c->type = cmp;
c->stride = sz;
List__init((List*)&c->cmps);
}
// add new component data to existing component pool
void* ECS__arch_cpool_add_cmp_data(CArch arch, u16 cmp_idx, CCmp cmp, void* data) {
Archetype* a = &_G->world.archs[arch];
ComponentPool* c = &a->cpools[cmp_idx];
a->dirty |= OBS_SET;
return ECS__page_record_add((List*)&c->cmps, CPOOL_PAGE_COUNT, c->stride, data);
}
// set dirty flag (when existing component data was updated)
void ECS__arch_cmp_updated(CArch arch) {
Archetype* a = &_G->world.archs[arch];
a->dirty |= OBS_SET;
}
// check if dirty flag is set on existing archetype
inline bool ECS__is_dirty(CArch arch, CObs event) {
Archetype* a = &_G->world.archs[arch];
return 0 != (a->dirty & event);
}
// clear dirty flags on all archetypes (to be called at end of frame)
void ECS__clean() {
Archetype* archs = _G->world.archs;
for (u16 i = 0; i < ARCH_COUNT; i++) {
archs[i].dirty = OBS_NONE;
}
}
// TODO: make possible to identify which component in a pool was ADD|REMOVE|SET
// Queries
// iterator initialization before for..loop
inline ArchIt* ECS__query_init(ArchIt* it) {
Archetype* a = &_G->world.archs[it->arch];
it->a = a;
it->len = a->en_ct;
return it;
}
// iterator updates inside for..loop
inline void ECS__query_iter(u16 i, ArchIt* it, ...) {
// TODO: eventually need to be able to walk multiple archetypes in one loop, or multiple calls to one static system_cb fn
// TODO: query will need to skip/purge dead entity cmp data (and eventually force rebuild of archetype) (ie. call ECS__defrag/sweep() fn to centralize/control/debug-count it)
if (0 != i % CPOOL_PAGE_COUNT) {
return;
}
// TODO: optimize this asm to be as few ops as possible!
// event: page roll-over
it->page_idx = i / CPOOL_PAGE_COUNT;
it->rec_idx = i % CPOOL_PAGE_COUNT;
va_list args;
va_start(args, it);
PagedEnt* p1;
PagedCmp* p2;
p1 = List__get((List*)&it->a->entities, it->page_idx);
it->eid = p1->records;
for (u8 i = 0; i < it->a->cmp_ct; i++) {
void** ptr = va_arg(args, void**);
p2 = List__get((List*)&it->a->cpools[i].cmps, it->page_idx);
*ptr = p2->records;
}
va_end(args);
}
// Convenience Wrappers
// allocate a new archetype table
CArch ECS__arch(CID cmp_ct, CArch arch, ...) {
ECS__arch_alloc(arch, cmp_ct);
va_list args;
va_start(args, arch);
for (u8 i = 0; i < cmp_ct * 2; i += 2) {
CCmp cls = va_arg(args, CCmp);
u32 sz = va_arg(args, u32);
ECS__arch_cpool_alloc(arch, cls, sz);
}
va_end(args);
return arch;
}
// allocate a new entity into the world, w/ all its component data
GID ECS__entity(CArch arch, ...) {
Archetype* a = &_G->world.archs[arch];
GID r = GI_get_next_id(arch);
ECS__arch_ent_add(r);
va_list args;
va_start(args, arch);
for (u8 i = 0; i < a->cmp_ct; i++) {
void* pCmp = va_arg(args, void*);
ECS__arch_cpool_add_cmp_data(arch, i, 0, pCmp);
}
va_end(args);
return r;
}
// return pointer to component data array for given entity
void* ECS__field(GID entity, u8 cmp_idx) {
CArch arch = GI_arch(entity);
Archetype* a = &_G->world.archs[arch];
CID arch_idx = GI_arch_idx(entity);
u8 page_idx = arch_idx / CPOOL_PAGE_COUNT;
u8 rec_idx = arch_idx % CPOOL_PAGE_COUNT;
ComponentPool* cpool = &a->cpools[cmp_idx];
PagedCmp* p2 = List__get((List*)&cpool->cmps, page_idx);
void* r = p2->records + (cpool->stride * rec_idx);
return r;
}
// this is the actual in-game usage example
// clone a new cat entity instance from this prefab
GID CatEntity__prefab(f32 x, f32 y, f32 z, u16 r) {
// construct entity
CArch a = A_CTRANSFORM3D_CRIGIDBODY2D_CREPLICA_CBRAIN_CRENDER;
GID cat = GI_get_next_id(a);
CmpTransform3D tf = {x, y, z, r};
CmpRigidbody2D rb = {0.0, 0.0, 0U};
CmpReplica replica = {
.net_eid = _G->isMaster ? cat : 0,
.world_static = false,
};
CmpBrain brain = {
.bb = NULL,
};
CmpRender render = {0};
render.rg = WORLD_ZSORT_RG;
render.billboard = true;
render.material = Material__alloc(&_G->materials.sprite);
Preload(&_G->models.plane2D, &(Str8){"../assets/models/plane2D.obj"});
render.material->mesh = _G->models.plane2D;
Preload(&_G->images.atlas, &(Str8){"../assets/textures/atlas.bmp"});
render.material->shader = &_G->shaders.atlas;
render.material->texture0 = _G->images.atlas;
render.tw = render.th = 8;
render.aw = render.ah = 64;
render.pi = 0;
render.po = Math__randomu(0, 7, &_G->seeds.sg);
render.ti = 4 + 2 * 8;
render.useMask = true;
render.mask = COLOR_BLACK;
render.color = COLOR_TRANSPARENT;
ECS__arch_ent_add(cat);
ECS__arch_cpool_add_cmp_data(a, 0, CTRANSFORM3D, &tf);
ECS__arch_cpool_add_cmp_data(a, 1, CRIGIDBODY2D, &rb);
ECS__arch_cpool_add_cmp_data(a, 2, CREPLICA, &replica);
ECS__arch_cpool_add_cmp_data(a, 3, CBRAIN, &brain);
ECS__arch_cpool_add_cmp_data(a, 4, CRENDER, &render);
Preload(&_G->audio.meow, &(Str8){"../assets/audio/sfx/meow.wav"});
return cat;
}
// this is the unit test
// @describe ECS
// @tag common
int main() {
_G->arena = Arena__Alloc(50 * 1024 * 1024); // MB
//---
// Scenario: Generational Index
GI_init();
// clang-format off
const CArch a1 = ECS__arch(2,
A_CTRANSFORM3D_CRIGIDBODY2D,
CTRANSFORM3D, sizeof(CmpTransform3D),
CRIGIDBODY2D, sizeof(CmpRigidbody2D));
// clang-format on
GID eid0 = GI_eid(false, 1, 2, 3);
ASSERT(!GI_isPair(eid0));
ASSERT(1 == GI_gen(eid0));
ASSERT(2 == GI_arch(eid0));
ASSERT(3 == GI_arch_idx(eid0));
eid0 = GI_eid(true, 1, 2, 3);
ASSERT(GI_isPair(eid0));
eid0 = GI_get_next_id(a1);
ASSERT(0 == GI_gen(eid0));
ASSERT(a1 == GI_arch(eid0));
ASSERT(0 == GI_arch_idx(eid0));
GID eid1 = GI_get_next_id(a1);
ASSERT(0 == GI_gen(eid1));
ASSERT(a1 == GI_arch(eid1));
ASSERT(1 == GI_arch_idx(eid1));
GID eid2 = GI_get_next_id(a1);
ASSERT(0 == GI_gen(eid2));
ASSERT(a1 == GI_arch(eid2));
ASSERT(2 == GI_arch_idx(eid2));
GI_set_dead(eid1);
ASSERT(false == GI_is_alive(eid1));
GID eid3 = GI_get_next_id(a1);
ASSERT(1 == GI_gen(eid3));
ASSERT(a1 == GI_arch(eid3));
ASSERT(1 == GI_arch_idx(eid3));
GID eid4 = GI_get_next_id(a1);
ASSERT(0 == GI_gen(eid4));
ASSERT(a1 == GI_arch(eid4));
ASSERT(3 == GI_arch_idx(eid4));
ASSERT(false == GI_is_alive(eid1));
ASSERT(true == GI_is_alive(eid2));
ASSERT(true == GI_is_alive(eid3));
ASSERT(true == GI_is_alive(eid4));
// ---
// Scenario: Entity + Component creation
// LOG_DEBUGF("Tag (PREFABS) %u", TPREFABS);
// LOG_DEBUGF("Tag (PLAYERS) %u", TPLAYERS);
// LOG_DEBUGF("Tag (APPLES) %u", TAPPLES);
// LOG_DEBUGF("Tag (LIKES) %u", TLIKES);
// LOG_DEBUGF("Tag (EATS) %u", TEATS);
GI_init(); // reset world generations
ASSERT(!ECS__is_dirty(a1, OBS_ADD));
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE));
ASSERT(!ECS__is_dirty(a1, OBS_SET));
GID alice = GI_get_next_id(a1);
ECS__arch_ent_add(alice);
ECS__arch_cpool_add_cmp_data(a1, 0, CTRANSFORM3D, &(CmpTransform3D){1.0f, 1.0f, 1.0f, 45U});
ECS__arch_cpool_add_cmp_data(a1, 1, CRIGIDBODY2D, &(CmpRigidbody2D){0.0f, 0.0f, 1.0f, 0U});
ASSERT(ECS__is_dirty(a1, OBS_ADD));
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE));
ASSERT(ECS__is_dirty(a1, OBS_SET));
ECS__clean();
ASSERT(!ECS__is_dirty(a1, OBS_ADD));
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE));
ASSERT(!ECS__is_dirty(a1, OBS_SET));
ECS__arch_cmp_updated(a1);
ASSERT(!ECS__is_dirty(a1, OBS_ADD));
ASSERT(!ECS__is_dirty(a1, OBS_REMOVE));
ASSERT(ECS__is_dirty(a1, OBS_SET));
ECS__clean();
{ // System
ArchIt* it = ECS__query_init(&(ArchIt){a1});
CmpTransform3D* tf;
CmpRigidbody2D* rb;
for (u16 i = 0; i < it->len; i++) {
ECS__query_iter(i, it, &tf, &rb);
LOG_DEBUGF(
"arch %u en %u gen %u tf %f %f %f %u rb %f %f %u",
GI_arch(it->eid[i]),
GI_arch_idx(it->eid[i]),
GI_gen(it->eid[i]),
tf[i].x,
tf[i].y,
tf[i].z,
tf[i].rot,
rb[i].vx,
rb[i].vy,
rb[i].avel);
}
}
ECS__arch_ent_del(alice);
ASSERT(!ECS__is_dirty(a1, OBS_ADD));
ASSERT(ECS__is_dirty(a1, OBS_REMOVE));
ASSERT(!ECS__is_dirty(a1, OBS_SET));
ECS__clean();
// ---
// Scenario: Application to Meow Game
// clang-format off
const CArch a2 = ECS__arch(3,
A_CTRANSFORM3D_CRIGIDBODY2D_CCOOLDOWN,
CTRANSFORM3D, sizeof(CmpTransform3D),
CRIGIDBODY2D, sizeof(CmpRigidbody2D),
CCOOLDOWN, sizeof(CmpCooldown));
// clang-format on
GID cat1 = ECS__entity(
a2,
&(CmpTransform3D){1.0f, 1.0f, 1.0f, 0U},
&(CmpRigidbody2D){0.0f, 0.0f, 0U},
CD_cmp_alloc(&(CmpCooldown){0}, CD_CAT_COUNT));
// read cmp
CmpTransform3D* tf = ECS__field(cat1, 0);
CmpRigidbody2D* rb = ECS__field(cat1, 1);
CmpCooldown* cd = ECS__field(cat1, 2);
GID cat2 = ECS__entity(
a2,
&(CmpTransform3D){2.0f, 2.0f, 2.0f, 0U},
&(CmpRigidbody2D){0.0f, 0.0f, 0U},
CD_cmp_alloc(&(CmpCooldown){0}, CD_CAT_COUNT));
return 0;
}
// these are the relevant structs / headers
// Single Compilation Unit -------------
#define GENERIC_LIST(N, T) \
typedef struct N##__Node { \
struct N##__Node* next; \
T data; \
} N##__Node; \
typedef struct { \
u32 len; \
N##__Node* head; \
N##__Node* tail; \
} N;
GENERIC_LIST(List, void*)
// BEGIN ECS --------------------------------------------
#define L1_CACHE_LINE (64) // bytes (per variable-read)
// (4B) GID x (1024) live-entities = 4KB GenIdx
#define ENT_MAX (1024) // live entity limit (up to arch_idx cap)
typedef u32 GID; // Generational Index Id
typedef u16 CID; // Live Entity Counter (up to ENT_MAX)
// Entity Id case
// (1) pair = 0
// (1) alive
// (10) gen 1024 generations
// (10) arch 1024 archetypes
// (10) arch_idx 1024 archetype entity offset
// (gen x arch x arch_idx = 1 bil dead, 1 mil live)
//
// Pair Id case
// (1) pair = 1
// (1) data (included w/ relationship)
// (10) src 1024 tag sources
// (10) rel 1024 tag relationships
// (10) dst 1024 tag destinations
#define PAIR_BIT (0x80000000) // 10000000 00000000 00000000 00000000
// #define ALIVE_BIT (0x40000000) // 01000000 00000000 00000000 00000000
#define GEN_SHIFT (20) // bits
#define GEN_MASK (0x3ff00000) // 00111111 11110000 00000000 00000000
#define ARCH_SHIFT (10) // bits
#define ARCH_MASK (0x000ffc00) // 00000000 00001111 11111100 00000000
#define ARCH_IDX_MASK (0x000003ff) // 00000000 00000000 00000011 11111111
// magic number for system queries of pairs;
// (one unlikely to be used naturally,
// but still fits in EID type)
#define ANY (1024) // ENT_IDX_BITS + 1
#define CPOOL_PAGE_COUNT (64) // number of records per page of a component pool
typedef struct {
GID a[ENT_MAX]; // array
CID alive_ct; // count
CID dead_ct; // count
CID dead_head_idx; // index
} GenIdx; // Generational Index for ECS
// #ifdef DEBUG_SLOW
typedef struct {
bool pair;
// bool unused;
CID gen;
CID arch;
CID arch_idx;
} DebugGenId;
// #endif
#define GENERIC_PAGED(N, T) \
typedef struct { \
T records; \
CID count; \
} N;
GENERIC_PAGED(Paged, void*)
GENERIC_PAGED(PagedEnt, GID*)
GENERIC_LIST(ListPagedEnt, PagedEnt*)
typedef struct {
List__Node* node;
CID row_idx;
void* record;
} PageIt; // Page Iterator
typedef enum {
A_CTRANSFORM3D_CRIGIDBODY2D, // used by unit test only
A_CTRANSFORM3D_CRIGIDBODY2D_CCOOLDOWN, // used by unit test only
A_CTRANSFORM3D_CRIGIDBODY2D_CREPLICA_CBRAIN_CRENDER, // used in-game
// ... // (so far; expecting more)
ARCH_COUNT,
} CArch;
typedef enum {
CDEBUG,
CTRANSFORM3D,
CRIGIDBODY2D,
CCOOLDOWN,
CREPLICA,
CBRAIN,
CRENDER,
CMP_COUNT,
} CCmp;
typedef enum {
OBS_NONE = 0,
// at least one entity was added to the archetype
OBS_ADD = 1 << 1,
// at least one entity was removed from the archetype
OBS_REMOVE = 1 << 2,
// at least one entity's component data was added/updated in the archetype (first ADD will exec, then SET)
OBS_SET = 1 << 3,
OBS_COUNT,
} CObs;
GENERIC_PAGED(PagedCmp, void*)
GENERIC_LIST(ListPagedCmp, PagedCmp*)
typedef struct {
CCmp type;
u8 stride; // size of one component (should be small!)
ListPagedCmp cmps; // list of Pages of component structs
} ComponentPool;
typedef struct {
CArch type;
CID en_ct;
ListPagedEnt entities; // list of Pages of EntityIDs
u8 cmp_ct;
ComponentPool* cpools; // array of ptr to ComponentPools (SoA)
CObs dirty; // observer change flags
} Archetype;
typedef struct {
CArch arch;
Archetype* a;
GID* eid;
CID len;
u8 page_idx; // shouldn't be many
u8 rec_idx; // shouldn't be many
} ArchIt;
typedef struct {
GenIdx gi;
Archetype archs[ARCH_COUNT]; // array of Archetype
} World;
// Components --------------------------------------------
typedef struct {
Cooldown* set;
} CmpCooldown;
typedef struct {
f32 x, y, z;
u16 rot; // yaw, Euler
} CmpTransform3D;
typedef struct {
f32 mass; // kg (mass x velocity = momentum)
// delta momentum, represents the transfer/conservation of momentum
f32 vx, vy; // Linear velocity (Force x deltaTime = impulse)
u8 avel; // Angular velocity (radians/s)
} CmpRigidbody2D;
// END ECS ----------------------------------------------
@mikesmullin
Copy link
Author

Copyright 2025 Mike Smullin

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment