diff --git a/art/gen/enemy1.png b/art/gen/enemy1.png new file mode 100644 index 0000000..1f46699 Binary files /dev/null and b/art/gen/enemy1.png differ diff --git a/code/foundation/CMakeLists.txt b/code/foundation/CMakeLists.txt index 88b3d4d..ef5a4f5 100644 --- a/code/foundation/CMakeLists.txt +++ b/code/foundation/CMakeLists.txt @@ -45,6 +45,6 @@ add_library(eco2d-foundation STATIC target_compile_definitions(eco2d-foundation PRIVATE CLIENT) include_directories(src ../modules ../../art/gen) -target_link_libraries(eco2d-foundation raylib raylib_nuklear cwpack flecs-bundle vendors-bundle) +target_link_libraries(eco2d-foundation raylib raylib_nuklear ferox cwpack flecs-bundle vendors-bundle) link_system_libs(eco2d-foundation) diff --git a/code/foundation/src/core/rules.h b/code/foundation/src/core/rules.h index 28e9954..0ecf5ac 100644 --- a/code/foundation/src/core/rules.h +++ b/code/foundation/src/core/rules.h @@ -24,6 +24,8 @@ typedef struct { float vehicle_brake_force; float veh_enter_radius; float blueprint_build_time; + + // survival rules } game_rulesdef; extern game_rulesdef game_rules; diff --git a/code/foundation/src/dev/debug_ui.c b/code/foundation/src/dev/debug_ui.c index de6a66d..aef5d32 100644 --- a/code/foundation/src/dev/debug_ui.c +++ b/code/foundation/src/dev/debug_ui.c @@ -163,7 +163,8 @@ static debug_item items[] = { .is_collapsed = false } }, - { .kind = DITEM_BUTTON, .name = "spawn car", .on_click = ActSpawnCar }, + { .kind = DITEM_BUTTON, .name = "spawn mobs", .on_click = ActSpawnMobs }, + { .kind = DITEM_BUTTON, .name = "spawn car", .on_click = ActSpawnCar }, { .kind = DITEM_BUTTON, .name = "place ice rink", .on_click = ActPlaceIceRink }, { .kind = DITEM_BUTTON, .name = "erase world changes", .on_click = ActEraseWorldChanges }, { .kind = DITEM_BUTTON, .name = "spawn circling driver", .on_click = ActSpawnCirclingDriver }, diff --git a/code/foundation/src/dev/debug_ui_actions.c b/code/foundation/src/dev/debug_ui_actions.c index 1b8e540..e310694 100644 --- a/code/foundation/src/dev/debug_ui_actions.c +++ b/code/foundation/src/dev/debug_ui_actions.c @@ -54,6 +54,32 @@ ActSpawnSelItem(void) { entity_set_position(e, origin->x, origin->y); } +void +ActSpawnMobs(void) { + ecs_entity_t plr = camera_get().ent_id; + Position const* origin = ecs_get(world_ecs(), plr, Position); + + const uint32_t w = 12*WORLD_BLOCK_SIZE; + const uint32_t h = 12*WORLD_BLOCK_SIZE; + uint32_t x = (uint32_t)origin->x - w/2; + uint32_t y = (uint32_t)origin->y - h/2; + + for (uint32_t cy=y; cy= world_dim()) continue; + if (cy < 0 || cy >= world_dim()) continue; + + if ((cy == y || cy == (y + h-WORLD_BLOCK_SIZE)) || + (cx == x || cx == (x + w-WORLD_BLOCK_SIZE))) { + ecs_entity_t e = entity_spawn_id(ASSET_MOB); + entity_set_position(e, (float)cx, (float)cy); + + ecs_add(world_ecs(), e, MobMelee); + } + } + } +} + void ActSpawnCirclingDriver(void) { ecs_entity_t plr = camera_get().ent_id; diff --git a/code/foundation/src/dev/debug_ui_tools.c b/code/foundation/src/dev/debug_ui_tools.c index 574ec81..63b6298 100644 --- a/code/foundation/src/dev/debug_ui_tools.c +++ b/code/foundation/src/dev/debug_ui_tools.c @@ -23,6 +23,14 @@ void ToolAssetInspector(void) { tooltip_show_cursor(asset_names[i]); } + if (nk_button_label(dev_ui, "spawn at me")) { + uint64_t e = entity_spawn_id(i); + ecs_entity_t plr = camera_get().ent_id; + + Position const* origin = ecs_get(world_ecs(), plr, Position); + entity_set_position(e, origin->x, origin->y); + } + // draw help text if (nk_tree_push_id(dev_ui, NK_TREE_NODE, "description", NK_MINIMIZED, i)) { { diff --git a/code/foundation/src/gen/texgen_fallback.c b/code/foundation/src/gen/texgen_fallback.c index e1c4f55..64562e4 100644 --- a/code/foundation/src/gen/texgen_fallback.c +++ b/code/foundation/src/gen/texgen_fallback.c @@ -56,6 +56,9 @@ Texture2D texgen_build_sprite_fallback(asset_id id) { case ASSET_CRAFTBENCH: return LoadTexEco("craftbench"); case ASSET_SPLITTER: return LoadTexEco("item_splitter"); case ASSET_ASSEMBLER: return LoadTexEco("assembler"); + + // Mobs + case ASSET_MOB: return LoadTexEco("enemy1"); default: break; } diff --git a/code/foundation/src/gui/tooltip.c b/code/foundation/src/gui/tooltip.c index 2c1e0e9..881df95 100644 --- a/code/foundation/src/gui/tooltip.c +++ b/code/foundation/src/gui/tooltip.c @@ -60,6 +60,7 @@ void tooltip_register_defaults(void) { tooltip_register( (tooltip) { .name = "ASSET_IRON_ORE", .content = "Natural resource that can be smelted in ASSET_FURNACE." } ); tooltip_register( (tooltip) { .name = "ASSET_IRON_INGOT", .content = "Used as a building material. It is smelted from ASSET_IRON_ORE." } ); tooltip_register( (tooltip) { .name = "ASSET_SCREWS", .content = "Used as a building material. It is crafted from ASSET_IRON_PLATES." } ); + tooltip_register( (tooltip) { .name = "ASSET_MOB", .content = "Enemy hunting player down." } ); tooltip_register( (tooltip) { .name = "craft", .content = "Crafting is the process of constructing tools, items, and blocks." } ); tooltip_register( (tooltip) { .name = "smelt", .content = "Smelting is a process of applying heat to ore, to extract a base metal. It is a form of extractive metallurgy. It is used to extract many metals from their ores, including silver, iron, copper, and other base metals." } ); } @@ -89,6 +90,8 @@ tooltip *tooltip_find_desc(const char *name) { } const char *tooltip_find_desc_contents(const char *name) { + if (!tooltips) return 0; + for (zpl_isize i = 0; i < zpl_array_count(tooltips); ++i) { tooltip *tp = (tooltips + i); diff --git a/code/foundation/src/lists/assets_ids.h b/code/foundation/src/lists/assets_ids.h index 14981e0..81b6cbb 100644 --- a/code/foundation/src/lists/assets_ids.h +++ b/code/foundation/src/lists/assets_ids.h @@ -20,6 +20,7 @@ X(ASSET_BLUEPRINT)\ X(ASSET_BLUEPRINT_DEMO_HOUSE)\ X(ASSET_BLUEPRINT_END)\ + X(ASSET_MOB)\ X(ASSET_FENCE)\ X(ASSET_DEV)\ X(ASSET_GROUND)\ diff --git a/code/foundation/src/lists/assets_list.c b/code/foundation/src/lists/assets_list.c index fca3059..6b5f191 100644 --- a/code/foundation/src/lists/assets_list.c +++ b/code/foundation/src/lists/assets_list.c @@ -31,6 +31,7 @@ static asset assets[] = { ASSET_TEX(ASSET_CRAFTBENCH), ASSET_TEX(ASSET_BLUEPRINT), ASSET_TEX(ASSET_BLUEPRINT_DEMO_HOUSE), + ASSET_TEX(ASSET_MOB), // NOTE(zaklaus): blocks ASSET_TEX(ASSET_FENCE), diff --git a/code/foundation/src/lists/entity_spawnlist.c b/code/foundation/src/lists/entity_spawnlist.c index 8515139..03adc14 100644 --- a/code/foundation/src/lists/entity_spawnlist.c +++ b/code/foundation/src/lists/entity_spawnlist.c @@ -12,6 +12,7 @@ static struct { { .id = ASSET_ASSEMBLER, .proc = assembler_spawn }, { .id = ASSET_BLUEPRINT, .proc_udata = blueprint_spawn_udata }, { .id = ASSET_CREATURE, .proc = creature_spawn }, + { .id = ASSET_MOB, .proc = mob_spawn }, }; #define MAX_ENTITY_SPAWNDEFS ((sizeof(entity_spawnlist))/(sizeof(entity_spawnlist[0]))) diff --git a/code/foundation/src/lists/prefabs_list.c b/code/foundation/src/lists/prefabs_list.c deleted file mode 100644 index e69de29..0000000 diff --git a/code/foundation/src/models/components.h b/code/foundation/src/models/components.h index b7a1d08..3282106 100644 --- a/code/foundation/src/models/components.h +++ b/code/foundation/src/models/components.h @@ -36,7 +36,41 @@ typedef struct { } Drawable; typedef Vector2D Position; -typedef Vector2D Velocity; +typedef struct { + float x; + float y; + float m; +} Velocity; + +enum { + PHYS_CIRCLE, + PHYS_RECT +}; + +typedef struct { + uint8_t kind; + union { + struct { + float r; + } circle; + + struct { + float w; + float h; + } rect; + }; + + float density; + float static_friction; + float dynamic_friction; + + // flags + uint8_t inf_inertia:4; + uint8_t inf_mass:4; + + // internals + uintptr_t body_ptr; +} PhysicsBody; typedef struct { float x; @@ -81,12 +115,20 @@ typedef struct { typedef struct { float hp; float max_hp; - - //NOTE(zaklaus): Intentionally global, to allow for creative use of damage combos - float pain_time; - float heal_time; } Health; +typedef struct { + float amt; +} HealthRegen; + +typedef struct { + uint8_t delay; +} HealDelay; + +typedef struct { + uint8_t _unused; +} HealthDecreased; + typedef struct { uint16_t id; } Classify; @@ -194,15 +236,34 @@ typedef struct { typedef struct { char _unused; } SeeksFood; typedef struct { char _unused; } SeeksCompanion; +// survival comps +typedef struct { + uint8_t atk_delay; +} Mob; +typedef struct { + uint64_t plr; +} MobHuntPlayer; + +typedef struct { + char _unused; +} MobMelee; + #define _COMPS\ X(Vector2D)\ X(Position)\ X(Velocity)\ + X(PhysicsBody)\ X(Chunk)\ X(Drawable)\ X(Input)\ X(ClientInfo)\ X(Health)\ + X(HealthRegen)\ + X(HealDelay)\ + X(HealthDecreased)\ + X(Mob)\ + X(MobHuntPlayer)\ + X(MobMelee)\ X(Classify)\ X(Vehicle)\ X(IsInVehicle)\ diff --git a/code/foundation/src/models/entity.c b/code/foundation/src/models/entity.c index 7fad813..3c76a57 100644 --- a/code/foundation/src/models/entity.c +++ b/code/foundation/src/models/entity.c @@ -19,7 +19,7 @@ uint64_t entity_spawn(uint16_t class_id) { if (class_id != EKIND_SERVER) { librg_entity_track(world_tracker(), e); - ecs_set(world_ecs(), e, Velocity, {0}); + ecs_set(world_ecs(), e, Velocity, {.x = 0, .y = 0, .m = 500.0f}); entity_set_position(e, (float)(rand() % world_dim()), (float)(rand() % world_dim())); librg_entity_owner_set(world_tracker(), e, (int64_t)e); @@ -70,6 +70,7 @@ void entity_despawn(uint64_t ent_id) { } void entity_set_position(uint64_t ent_id, float x, float y) { + ecs_set(world_ecs(), ent_id, Position, {x, y}); Position *p = ecs_get_mut_ex(world_ecs(), ent_id, Position); p->x = x; p->y = y; diff --git a/code/foundation/src/models/prefabs/player.c b/code/foundation/src/models/prefabs/player.c index c466752..f5b1808 100644 --- a/code/foundation/src/models/prefabs/player.c +++ b/code/foundation/src/models/prefabs/player.c @@ -16,10 +16,21 @@ uint64_t player_spawn(char *name) { } ecs_set_name(world_ecs(), e, name); + ecs_set(world_ecs(), e, PhysicsBody, { + .kind = PHYS_CIRCLE, + .circle.r = 1.5f, + .density = 0.5f, + .static_friction = 0.35f, + .dynamic_friction = 0.15f, + .inf_inertia = 1, + }); ecs_set(world_ecs(), e, ClientInfo, {0}); ecs_set(world_ecs(), e, Input, {0}); ecs_set(world_ecs(), e, Inventory, {0}); - ecs_set(world_ecs(), e, Health, {.hp = PLAYER_MAX_HP, .max_hp = PLAYER_MAX_HP}); + ecs_set(world_ecs(), e, Health, {.hp = PLAYER_MAX_HP, .max_hp = PLAYER_MAX_HP}); + ecs_set(world_ecs(), e, HealthRegen, {.amt = 15.0f}); + Velocity *v = ecs_get_mut(world_ecs(), e, Velocity); + *v = (Velocity) { 0, 0, 0.0f }; librg_entity_owner_set(world_tracker(), e, (int64_t)e); diff --git a/code/foundation/src/models/prefabs/prefabs_list.c b/code/foundation/src/models/prefabs/prefabs_list.c index c757478..6a9cce7 100644 --- a/code/foundation/src/models/prefabs/prefabs_list.c +++ b/code/foundation/src/models/prefabs/prefabs_list.c @@ -116,4 +116,22 @@ uint64_t storage_spawn(void) { return (uint64_t)e; } -//------------------------------------------------------------------------ \ No newline at end of file +//------------------------------------------------------------------------ + +uint64_t mob_spawn(void) { + ecs_entity_t e = entity_spawn(EKIND_MONSTER); + + Health *hp = ecs_get_mut_ex(world_ecs(), e, Health); + hp->max_hp = hp->hp = 100.0f; + + ecs_add(world_ecs(), e, Mob); + ecs_set(world_ecs(), e, PhysicsBody, { + .kind = PHYS_CIRCLE, + .circle.r = 1.5f, + .density = 0.25f, + .static_friction = 0.35f, + .dynamic_friction = 0.15f + }); + + return (uint64_t)e; +} diff --git a/code/foundation/src/systems/modules/system_mob.c b/code/foundation/src/systems/modules/system_mob.c new file mode 100644 index 0000000..3e47805 --- /dev/null +++ b/code/foundation/src/systems/modules/system_mob.c @@ -0,0 +1,79 @@ +void MobDetectPlayers(ecs_iter_t *it) { + Position *p = ecs_field(it, Position, 1); + + for (int i = 0; i < it->count; i++) { + float closest_ent_dist = ZPL_F32_MAX; + uint64_t closest_ent = 0; + + ecs_iter_t pit = ecs_query_iter(world_ecs(), world_ecs_player()); + + while (ecs_query_next(&pit)) { + Position *p2 = ecs_field(&pit, Position, 2); + + for (int j = 0; j < pit.count; j++) { + float dx = p2->x - p[j].x; + float dy = p2->y - p[j].y; + float range = (dx*dx + dy*dy); + + if (range < closest_ent_dist) + closest_ent = pit.entities[j]; + } + } + + if (!closest_ent) + continue; + + ecs_set(it->world, it->entities[i], MobHuntPlayer, { .plr = closest_ent }); + } +} + +#define MOB_MOVEMENT_SPEED 300.0f + +void MobMovement(ecs_iter_t *it) { + Velocity *v = ecs_field(it, Velocity, 1); + Position *p = ecs_field(it, Position, 2); + MobHuntPlayer *m = ecs_field(it, MobHuntPlayer, 3); + + for (int i = 0; i < it->count; i++) { + const Position *p2 = ecs_get(it->world, m->plr, Position); + zpl_vec2 pos1 = { .x = p[i].x, .y = p[i].y }; + zpl_vec2 pos2 = { .x = p2->x, .y = p2->y }; + zpl_vec2 dir; + zpl_vec2_sub(&dir, pos2, pos1); + zpl_vec2_norm(&dir, dir); + + v[i].x += dir.x*(MOB_MOVEMENT_SPEED*safe_dt(it)); + v[i].y += dir.y*(MOB_MOVEMENT_SPEED*safe_dt(it)); + + entity_wake(it->entities[i]); + } +} + +#define MOB_MELEE_DIST 4000.0f +#define MOB_MELEE_DMG 5.0f +#define MOB_ATK_DELAY 10 + +void MobMeleeAtk(ecs_iter_t *it) { + Position *p = ecs_field(it, Position, 1); + Mob *mob = ecs_field(it, Mob, 2); + MobHuntPlayer *m = ecs_field(it, MobHuntPlayer, 3); + + for (int i = 0; i < it->count; i++) { + if (mob[i].atk_delay > 0) { + TICK_VAR(mob[i].atk_delay); + continue; + } + + const Position *p2 = ecs_get(it->world, m->plr, Position); + float dx = p2->x - p[i].x; + float dy = p2->y - p[i].y; + float range = (dx*dx + dy*dy); + + if (range < MOB_MELEE_DIST) { + Health *hp = ecs_get_mut_ex(it->world, m->plr, Health); + hp->hp = zpl_max(hp->hp-MOB_MELEE_DMG, 0.0f); + ecs_add(it->world, m->plr, HealthDecreased); + mob[i].atk_delay = MOB_ATK_DELAY; + } + } +} \ No newline at end of file diff --git a/code/foundation/src/systems/systems.c b/code/foundation/src/systems/systems.c index d5c6075..9614c8d 100644 --- a/code/foundation/src/systems/systems.c +++ b/code/foundation/src/systems/systems.c @@ -7,6 +7,9 @@ #include "dev/debug_draw.h" #include "core/game.h" #include "core/rules.h" +#include "ferox.h" + +extern frWorld *phys_world; #define PHY_BLOCK_COLLISION 1 #define PHY_WALK_DRAG 4.23f @@ -19,6 +22,7 @@ #include "modules/system_logistics.c" #include "modules/system_producer.c" #include "modules/system_blueprint.c" +#include "modules/system_mob.c" static inline float physics_correction(float x, float vx, float bounce, float dim) { float r = (((zpl_max(0.0f, dim - zpl_abs(x))*zpl_sign(x)))*dim); @@ -38,6 +42,7 @@ void IntegratePositions(ecs_iter_t *it) { if (ecs_get(it->world, it->entities[i], IsInVehicle)) { continue; } + //if (zpl_abs(v[i].x) >= 0.0001f || zpl_abs(v[i].y) >= 0.0001f) { // NOTE(zaklaus): world bounds @@ -108,7 +113,6 @@ void IntegratePositions(ecs_iter_t *it) { } } -#define HAZARD_BLOCK_TIME 1.0f #define HAZARD_BLOCK_DMG 5.0f void HurtOnHazardBlock(ecs_iter_t *it) { @@ -116,40 +120,50 @@ void HurtOnHazardBlock(ecs_iter_t *it) { Health *h = ecs_field(it, Health, 2); for (int i = 0; i < it->count; i++) { - if (h->pain_time < 0.0f) { - h->pain_time = HAZARD_BLOCK_TIME; - world_block_lookup l = world_block_from_realpos(p[i].x, p[i].y); - if (blocks_get_flags(l.bid) & BLOCK_FLAG_HAZARD) { - h->hp -= HAZARD_BLOCK_DMG; - h->hp = zpl_max(0.0f, h->hp); - } - } + world_block_lookup l = world_block_from_realpos(p[i].x, p[i].y); + if (blocks_get_flags(l.bid) & BLOCK_FLAG_HAZARD) { + h->hp -= HAZARD_BLOCK_DMG; + h->hp = zpl_max(0.0f, h->hp); + ecs_add(it->world, it->entities[i], HealthDecreased); + } } } -#define HP_REGEN_TIME 2.0f -#define HP_REGEN_PAIN_COOLDOWN 5.0f -#define HP_REGEN_RECOVERY 15.0f +//#define HP_REGEN_PAIN_COOLDOWN 5.0f void RegenerateHP(ecs_iter_t *it) { - Health *h = ecs_field(it, Health, 1); + Health *h = ecs_field(it, Health, 1); + HealthRegen *r = ecs_field(it, HealthRegen, 2); for (int i = 0; i < it->count; i++) { - if (h[i].pain_time < 0.0f) { - if (h[i].heal_time < 0.0f && h[i].hp < h[i].max_hp) { - h[i].heal_time = HP_REGEN_TIME; - h[i].hp += HP_REGEN_RECOVERY; - h[i].hp = zpl_min(h[i].max_hp, h[i].hp); - entity_wake(it->entities[i]); - } else { - h[i].heal_time -= safe_dt(it); - } - } else { - h[i].pain_time -= safe_dt(it); - } + // TODO delay regen on hurt + if (h[i].hp < h[i].max_hp) { + h[i].hp += r->amt; + h[i].hp = zpl_min(h[i].max_hp, h[i].hp); + entity_wake(it->entities[i]); + } } } +void OnHealthChangePutDelay(ecs_iter_t *it) { + for (int i = 0; i < it->count; i++) { + ecs_set(it->world, it->entities[i], HealDelay, { .delay = 10 }); + ecs_remove(it->world, it->entities[i], HealthDecreased); + } +} + +void TickDownHealDelay(ecs_iter_t *it) { + HealDelay *h = ecs_field(it, HealDelay, 1); + + for (int i = 0; i < it->count; i++) { + --h[i].delay; + + if (h[i].delay == 0) { + ecs_remove(it->world, it->entities[i], HealDelay); + } + } +} + void ResetActivators(ecs_iter_t *it) { Input *in = ecs_field(it, Input, 1); @@ -218,6 +232,86 @@ void PlayerClosestInteractable(ecs_iter_t *it){ } } +void PhysOnCreateBody(ecs_iter_t *it) { + PhysicsBody *pb = ecs_field(it, PhysicsBody, 1); + Position *p = ecs_field(it, Position, 2); + + for (int i = 0; i < it->count; i++) { + const frMaterial mat = { + .density = pb[i].density, + .staticFriction = pb[i].static_friction, + .dynamicFriction = pb[i].dynamic_friction, + }; + + frShape *shape = 0; + if (pb[i].kind == PHYS_CIRCLE) { + shape = frCreateCircle(mat, pb[i].circle.r); + } else { + shape = frCreateRectangle(mat, pb[i].rect.w, pb[i].rect.h); + } + + frBodyFlags flags = 0x0; + if (pb[i].inf_inertia) flags |= FR_FLAG_INFINITE_INERTIA; + if (pb[i].inf_mass) flags |= FR_FLAG_INFINITE_MASS; + frBody *body = frCreateBodyFromShape(FR_BODY_DYNAMIC, flags, frVec2PixelsToMeters((Vector2){p[i].x, p[i].y}), shape); + frAddToWorld(phys_world, body); + pb[i].body_ptr = (uintptr_t)body; + } +} + +void PhysOnRemoveBody(ecs_iter_t *it) { + PhysicsBody *pb = ecs_field(it, PhysicsBody, 1); + + for (int i = 0; i < it->count; i++) { + frBody *body = (frBody*)pb[i].body_ptr; + frRemoveFromWorld(phys_world, body); + frShape *shape = frGetBodyShape(body); + frReleaseBody(body); + frReleaseShape(shape); + } +} + +void PhysSetVelocity(ecs_iter_t *it) { + PhysicsBody *pb = ecs_field(it, PhysicsBody, 1); + Velocity *v = ecs_field(it, Velocity, 2); + + for (int i = 0; i < it->count; i++) { + frBody *body = (frBody*)pb[i].body_ptr; + frSetBodyVelocity(body, (Vector2) { v[i].x, v[i].y }); + } +} + +void PhysUpdatePosition(ecs_iter_t *it) { + PhysicsBody *pb = ecs_field(it, PhysicsBody, 1); + Position *p = ecs_field(it, Position, 2); + Velocity *v = ecs_field(it, Velocity, 3); + + for (int i = 0; i < it->count; i++) { + frBody *body = (frBody*)pb[i].body_ptr; + Vector2 pos = frVec2MetersToPixels(frGetBodyPosition(body)); + p[i].x = pos.x; + p[i].y = pos.y; + Vector2 vel = frVec2MetersToPixels(frGetBodyVelocity(body)); + v[i].x = vel.x; + v[i].y = vel.y; + } +} + +void PhysResetPosition(ecs_iter_t *it) { + Position *p = ecs_field(it, Position, 1); + + for (int i = 0; i < it->count; i++) { + const PhysicsBody *pb = ecs_get(it->world, it->entities[i], PhysicsBody); + if (!pb) continue; + frBody *body = (frBody*)pb->body_ptr; + frSetBodyPosition(body, (Vector2){p[i].x, p[i].y}); + } +} + +void PhysSimulateWorld(ecs_iter_t *it) { + frSimulateWorld(phys_world, it->delta_time); +} + void EnableWorldEdit(ecs_iter_t *it) { world_set_stage(it->world); } @@ -242,17 +336,23 @@ void SystemsImport(ecs_world_t *ecs) { ECS_MODULE(ecs, Systems); ecs_entity_t timer = ecs_set_interval(ecs, 0, ECO2D_TICK_RATE); - - ECS_SYSTEM(ecs, EnableWorldEdit, EcsOnLoad); + + ECS_SYSTEM(ecs, EnableWorldEdit, EcsOnLoad); ECS_SYSTEM(ecs, MovementImpulse, EcsOnLoad, components.Input, components.Velocity, components.Position, !components.IsInVehicle); ECS_SYSTEM(ecs, DemoNPCMoveAround, EcsOnLoad, components.Velocity, components.DemoNPC); ECS_SYSTEM(ecs, ApplyWorldDragOnVelocity, EcsOnUpdate, components.Position, components.Velocity); - ECS_SYSTEM_TICKED(ecs, HurtOnHazardBlock, EcsOnUpdate, components.Position, components.Health); - ECS_SYSTEM_TICKED(ecs, RegenerateHP, EcsOnUpdate, components.Health); + ECS_SYSTEM_TICKED_EX(ecs, HurtOnHazardBlock, EcsOnUpdate, 20.0f, components.Position, components.Health); + ECS_SYSTEM_TICKED_EX(ecs, RegenerateHP, EcsOnUpdate, 40.0f, components.Health, components.HealthRegen, !components.HealDelay); + ECS_SYSTEM_TICKED_EX(ecs, TickDownHealDelay, EcsOnUpdate, 20.0f, components.HealDelay); ECS_SYSTEM(ecs, VehicleHandling, EcsOnUpdate, components.Vehicle, components.Position, components.Velocity); + + ECS_OBSERVER(ecs, OnHealthChangePutDelay, EcsOnAdd, components.HealthDecreased); - ECS_SYSTEM(ecs, IntegratePositions, EcsOnValidate, components.Position, components.Velocity); + ECS_SYSTEM(ecs, PhysSetVelocity, EcsOnValidate, components.PhysicsBody, components.Velocity); + ECS_SYSTEM(ecs, PhysSimulateWorld, EcsOnValidate); + ECS_SYSTEM(ecs, PhysUpdatePosition, EcsOnValidate, components.PhysicsBody, components.Position, components.Velocity); + ECS_SYSTEM(ecs, IntegratePositions, EcsOnValidate, components.Position, components.Velocity, !components.PhysicsBody); ECS_SYSTEM(ecs, EnterVehicle, EcsPostUpdate, components.Input, components.Position, !components.IsInVehicle); ECS_SYSTEM(ecs, LeaveVehicle, EcsPostUpdate, components.Input, components.IsInVehicle, components.Velocity); @@ -275,11 +375,20 @@ void SystemsImport(ecs_world_t *ecs) { ECS_SYSTEM_TICKED(ecs, CreatureSeekFood, EcsPostUpdate, components.Creature, components.Position, components.Velocity, components.SeeksFood, !components.SeeksCompanion); ECS_SYSTEM_TICKED(ecs, CreatureSeekCompanion, EcsPostUpdate, components.Creature, components.Position, components.Velocity, components.SeeksCompanion, !components.SeeksFood); ECS_SYSTEM(ecs, CreatureRoamAround, EcsPostUpdate, components.Velocity, components.Creature, !components.SeeksFood, !components.SeeksCompanion); + + ECS_SYSTEM_TICKED_EX(ecs, MobDetectPlayers, EcsPostUpdate, 100.0f, components.Position, components.Mob); + ECS_SYSTEM(ecs, MobMovement, EcsPostUpdate, components.Velocity, components.Position, components.MobHuntPlayer); + ECS_SYSTEM_TICKED(ecs, MobMeleeAtk, EcsPostUpdate, components.Position, components.Mob, components.MobHuntPlayer, components.MobMelee); ECS_SYSTEM(ecs, ResetActivators, EcsPostUpdate, components.Input); ECS_SYSTEM(ecs, ClearVehicle, EcsUnSet, components.Vehicle); ECS_SYSTEM(ecs, ThrowItemsOut, EcsUnSet, components.ItemContainer, components.Position); + + // Physics hooks + ECS_OBSERVER(ecs, PhysOnCreateBody, EcsOnSet, components.PhysicsBody, components.Position); + ECS_OBSERVER(ecs, PhysOnRemoveBody, EcsUnSet, components.PhysicsBody); + ECS_OBSERVER(ecs, PhysResetPosition, EcsOnSet, components.Position); ECS_SYSTEM(ecs, DisableWorldEdit, EcsPostUpdate); diff --git a/code/foundation/src/world/world.c b/code/foundation/src/world/world.c index 44b86bd..d22c11c 100644 --- a/code/foundation/src/world/world.c +++ b/code/foundation/src/world/world.c @@ -12,9 +12,11 @@ #include "core/game.h" #include "models/entity.h" #include "models/crafting.h" +#include "ferox.h" #include "packets/pkt_send_librg_update.h" + #define ECO2D_STREAM_ACTIONFILTER 1 ZPL_TABLE(static, world_snapshot, world_snapshot_, entity_view); @@ -23,6 +25,7 @@ ZPL_TABLE(static, world_component_cache, world_component_cache_, zpl_uintptr); / static world_data world = {0}; static world_snapshot streamer_snapshot; static world_component_cache component_cache; +frWorld *phys_world = 0; entity_view *world_build_entity_view(int64_t e) { entity_view *cached_ev = world_snapshot_get(&streamer_snapshot, e); @@ -293,6 +296,18 @@ void world_free_worldgen_data(void) { world.outer_data = NULL; } +static inline +void world_init_physics(void) { + const Rectangle bounds = { + .x = -0.05f * frNumberPixelsToMeters(world_dim()), + .y = -0.05f * frNumberPixelsToMeters(world_dim()), + .width = 1.1f * frNumberPixelsToMeters(world_dim()), + .height = 1.1f * frNumberPixelsToMeters(world_dim()) + }; + + phys_world = frCreateWorld((Vector2) { 0.0f, 0.0f }, bounds); +} + int32_t world_init(int32_t seed, uint16_t chunk_size, uint16_t chunk_amount) { world.is_paused = false; world.seed = seed; @@ -309,6 +324,7 @@ int32_t world_init(int32_t seed, uint16_t chunk_size, uint16_t chunk_amount) { world_init_mapping(); world_chunk_setup_grid(); world_free_worldgen_data(); + world_init_physics(); zpl_printf("[INFO] Created a new server world\n"); @@ -328,6 +344,9 @@ int32_t world_destroy(void) { world_snapshot_destroy(&streamer_snapshot); world_component_cache_destroy(&component_cache); zpl_memset(&world, 0, sizeof(world)); + + frReleaseWorld(phys_world); + zpl_printf("[INFO] World was destroyed.\n"); return WORLD_ERROR_NONE; } @@ -428,6 +447,10 @@ ecs_world_t * world_ecs() { return world.ecs; } +ecs_query_t *world_ecs_player(void) { + return world.ecs_update; +} + ecs_query_t *world_ecs_clientinfo(void) { return world.ecs_clientinfo; } diff --git a/code/foundation/src/world/world.h b/code/foundation/src/world/world.h index 9316bd6..6b02709 100644 --- a/code/foundation/src/world/world.h +++ b/code/foundation/src/world/world.h @@ -3,6 +3,7 @@ #include "librg.h" #include "pkt/packet.h" #include "flecs.h" + #include "world/blocks.h" #define WORLD_ERROR_NONE +0x0000 @@ -62,6 +63,7 @@ int32_t world_write(pkt_header *pkt, void *udata); uint32_t world_buf(block_id const **ptr, uint32_t *width); uint32_t world_seed(void); ecs_world_t *world_ecs(void); +ecs_query_t *world_ecs_player(void); ecs_query_t *world_ecs_clientinfo(void); void world_set_stage(ecs_world_t *ecs); librg_world *world_tracker(void); diff --git a/code/games/CMakeLists.txt b/code/games/CMakeLists.txt index 9cb9dcb..f8a8d2f 100644 --- a/code/games/CMakeLists.txt +++ b/code/games/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(sandbox) add_subdirectory(minimal) +add_subdirectory(survival) diff --git a/code/games/survival/CMakeLists.txt b/code/games/survival/CMakeLists.txt new file mode 100644 index 0000000..ccfcd3b --- /dev/null +++ b/code/games/survival/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(survival + src/main.c + src/platform.c + src/worldgen.c + src/texgen.c + src/rules.c +) + +target_compile_definitions(minimal PRIVATE CLIENT) +include_directories(src ../../foundation/src ../../../art/gen) +target_link_libraries(survival eco2d-foundation) + +link_system_libs(survival) diff --git a/code/games/survival/src/main.c b/code/games/survival/src/main.c new file mode 100644 index 0000000..51c7d61 --- /dev/null +++ b/code/games/survival/src/main.c @@ -0,0 +1,95 @@ +#define ZPL_IMPL +#include "zpl.h" +#include "platform/system.h" +#include "core/game.h" +#include "models/entity.h" +#include "world/entity_view.h" +#include "utils/options.h" +#include "platform/signal_handling.h" +#include "platform/profiler.h" + +#include "flecs.h" +#include "flecs/flecs_os_api_stdcpp.h" + +#include "models/components.h" +#include "systems/systems.h" + +#include "platform/arch.h" + +#define DEFAULT_WORLD_SEED 302097 +#define DEFAULT_CHUNK_SIZE 16 /* amount of blocks within a chunk (single axis) */ +#define DEFAULT_WORLD_SIZE 5 /* amount of chunks within a world (single axis) */ + +/* + TODO + - monster spawner + - the longer we survive, the more and stronger enemies we spawn + - player grows HP by leveling up + - XP increases by killing mobs + - player can pick an "ability" upon reaching level milestones + - abilities: armor/shield, TODO ... + - enemies damage player when close to him in ticks (damage effects, ...) + - basic projectile pooling (flecs) + - somewhat believable world gen, small hamlets with cols, etc +*/ + +int main(int argc, char** argv) { + zpl_opts opts={0}; + zpl_opts_init(&opts, zpl_heap(), argv[0]); + + zpl_opts_add(&opts, "?", "help", "the HELP section", ZPL_OPTS_FLAG); + zpl_opts_add(&opts, "v", "viewer-only", "run viewer-only client", ZPL_OPTS_FLAG); + zpl_opts_add(&opts, "d", "server-only", "run dedicated server", ZPL_OPTS_FLAG); + zpl_opts_add(&opts, "p", "preview-map", "draw world preview", ZPL_OPTS_FLAG); + zpl_opts_add(&opts, "s", "seed", "world seed", ZPL_OPTS_INT); + zpl_opts_add(&opts, "r", "random-seed", "generate random world seed", ZPL_OPTS_FLAG); + //zpl_opts_add(&opts, "cs", "chunk-size", "amount of blocks within a chunk (single axis)", ZPL_OPTS_INT); + zpl_opts_add(&opts, "ws", "world-size", "amount of chunks within a world (single axis)", ZPL_OPTS_INT); + zpl_opts_add(&opts, "ip", "host", "host IP address", ZPL_OPTS_STRING); + zpl_opts_add(&opts, "port", "port", "port number", ZPL_OPTS_INT); + + uint32_t ok = zpl_opts_compile(&opts, argc, argv); + + if (!ok) { + zpl_opts_print_errors(&opts); + zpl_opts_print_help(&opts); + return -1; + } + + int8_t is_viewer_only = zpl_opts_has_arg(&opts, "viewer-only"); + int8_t is_server_only = zpl_opts_has_arg(&opts, "server-only"); + int32_t seed = (int32_t)zpl_opts_integer(&opts, "seed", DEFAULT_WORLD_SEED); + uint16_t world_size = (uint16_t)zpl_opts_integer(&opts, "world-size", DEFAULT_WORLD_SIZE); + uint16_t chunk_size = DEFAULT_CHUNK_SIZE; //zpl_opts_integer(&opts, "chunk-size", DEFAULT_CHUNK_SIZE); + zpl_string host = zpl_opts_string(&opts, "host", NULL); + uint16_t port = (uint16_t)zpl_opts_integer(&opts, "port", 0); + + game_kind play_mode = GAMEKIND_SINGLE; + + if (is_viewer_only) play_mode = GAMEKIND_CLIENT; + if (is_server_only) play_mode = GAMEKIND_HEADLESS; + + if (zpl_opts_has_arg(&opts, "random-seed")) { + zpl_random rnd={0}; + zpl_random_init(&rnd); + seed = zpl_random_gen_u32(&rnd); + zpl_printf("Seed: %u\n", seed); + } + + if (zpl_opts_has_arg(&opts, "preview-map")) { + generate_minimap(seed, WORLD_BLOCK_SIZE, chunk_size, world_size); + return 0; + } + + sighandler_register(); + game_init(host, port, play_mode, 1, seed, chunk_size, world_size, 0); + + game_run(); + + game_shutdown(); + sighandler_unregister(); + + zpl_string_free(host); + zpl_opts_free(&opts); + return 0; +} diff --git a/code/games/survival/src/platform.c b/code/games/survival/src/platform.c new file mode 100644 index 0000000..6eec1c8 --- /dev/null +++ b/code/games/survival/src/platform.c @@ -0,0 +1,131 @@ +#include "platform/platform.h" +#include "raylib.h" +#include "raymath.h" +#include "net/network.h" +#include "core/game.h" +#include "world/entity_view.h" +#include "world/prediction.h" +#include "core/camera.h" +#include "math.h" +#include "world/blocks.h" +#include "models/assets.h" +#include "platform/profiler.h" +#include "dev/debug_draw.h" +#include "dev/debug_ui.h" +#include "utils/raylib_helpers.h" +#include "platform/renderer.h" + +ZPL_DIAGNOSTIC_PUSH_WARNLEVEL(0) +#include "raylib-nuklear.h" +ZPL_DIAGNOSTIC_POP + +#define ARCH_IMPL +#include "platform/arch.h" + + +// NOTE(zaklaus): add-ins +#include "gui/ui_skin.c" +#include "gui/tooltip.c" +#include "gui/notifications.c" + +#include "renderer.c" + +void platform_init() { + platform_create_window("horde survival game"); + renderer_init(); + + notification_push("test1", "Hello World!"); +} + +void platform_shutdown() { + renderer_shutdown(); + CloseWindow(); +} + +void platform_input() { + float mouse_z = (GetMouseWheelMove()*0.5f); + float mouse_modified = target_zoom < 4 ? mouse_z / (zpl_exp(4 - (target_zoom))) : mouse_z; + + if (mouse_z != 0.0f) { + target_zoom = zpl_clamp(target_zoom + mouse_modified, 0.1f, 11.0f); + } + + + // NOTE(zaklaus): keystate handling + { + float x=0.0f, y=0.0f; + uint8_t use, sprint, ctrl; + if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) x += 1.0f; + if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) x -= 1.0f; + if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) y += 1.0f; + if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) y -= 1.0f; + + use = IsKeyPressed(KEY_SPACE); + sprint = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL); + + // NOTE(zaklaus): NEW! mouse movement + Vector2 mouse_pos = GetMousePosition(); + mouse_pos.x /= screenWidth; + mouse_pos.y /= screenHeight; + mouse_pos.x -= 0.5f; + mouse_pos.y -= 0.5f; + mouse_pos = Vector2Normalize(mouse_pos); + + if (game_get_kind() == GAMEKIND_SINGLE && IsMouseButtonDown(MOUSE_MIDDLE_BUTTON)) { + x = mouse_pos.x; + y = -mouse_pos.y; + } + + game_keystate_data in_data = { + .x = x, + .y = y, + .mx = mouse_pos.x, + .my = mouse_pos.y, + .use = use, + .sprint = sprint, + .ctrl = ctrl, + }; + + platform_input_update_input_frame(in_data); + } +} + +void platform_render() { + platform_resize_window(); + + profile(PROF_ENTITY_LERP) { + game_world_view_active_entity_map(lerp_entity_positions); + game_world_view_active_entity_map(do_entity_fadeinout); + } + + // HACK run once when player is dead + { + static char done = 0; + camera cam = camera_get(); + entity_view *e = game_world_view_active_get_entity(cam.ent_id); + if (e && e->hp <= 0.0f && !done) { + done = 1; + notification_push("DEAD", "YOU ARE DEAD!"); + } + } + + assets_frame(); + + BeginDrawing(); + { + profile (PROF_RENDER) { + renderer_draw(); + } + renderer_debug_draw(); + + debug_draw(); + notification_draw(); + game_draw_ui(); + } + EndDrawing(); + + if (request_shutdown) { + CloseWindow(); + } +} diff --git a/code/games/survival/src/renderer.c b/code/games/survival/src/renderer.c new file mode 100644 index 0000000..ab4aae3 --- /dev/null +++ b/code/games/survival/src/renderer.c @@ -0,0 +1,227 @@ +static Camera2D render_camera; +static float zoom_overlay_tran = 0.0f; +#include "ferox.h" + +#define CAM_OVERLAY_ZOOM_LEVEL 0.15f +#define ALPHA(x) ColorAlpha(x, data->tran_time) + +float zpl_lerp(float,float,float); +float zpl_to_degrees(float); + +void DrawNametag(const char* name, uint64_t key, entity_view *data, float x, float y) { + float size = 16.f; + float font_size = lerp(4.0f, 32.0f, 0.5f/(float)render_camera.zoom); + float font_spacing = 1.1f; + float title_bg_offset = 4; + float fixed_title_offset = 8.f; + float health = (data->hp / data->max_hp); + const char *title = TextFormat("%s %llu", name, key); + float title_w = MeasureTextEco(title, font_size, font_spacing); + DrawRectangleEco(x-title_w/2.f-title_bg_offset/2.f, y-size-font_size-fixed_title_offset, title_w+title_bg_offset, font_size, ColorAlpha(BLACK, data->tran_time)); + DrawRectangleEco(x-title_w/2.f-title_bg_offset/2.f, y-size-fixed_title_offset, title_w*health+title_bg_offset, font_size*0.2f, ColorAlpha(RED, data->tran_time)); + DrawTextEco(title, x-title_w/2.f, y-size-font_size-fixed_title_offset, font_size, ColorAlpha(RAYWHITE, data->tran_time), font_spacing); +} + +void renderer_draw_entry(uint64_t key, entity_view *data, game_world_render_entry* entry) { + float size = 16.f; + + switch (data->kind) { + case EKIND_CHUNK: { + world_view *view = game_world_view_get_active(); + float size = (float)(view->chunk_size * WORLD_BLOCK_SIZE); + float offset = 0.0; + + float x = data->x * size + offset; + float y = data->y * size + offset; + + if (entry == NULL) { + RenderTexture2D tex = GetChunkTexture(key); + float scale = (size)/(float)(tex.texture.width); + tex.texture.width *= (int32_t)scale; + tex.texture.height *= (int32_t)scale; + DrawTextureRec(tex.texture, (Rectangle){0, 0, size, -size}, (Vector2){x, y}, ColorAlpha(WHITE, data->tran_time)); + } else { + DrawTextureRec(GetBlockImage(entry->blk_id), ASSET_SRC_RECT(), (Vector2){entry->x-(WORLD_BLOCK_SIZE/2), entry->y-(WORLD_BLOCK_SIZE/2)}, ColorAlpha(WHITE, data->tran_time)); + } + }break; + case EKIND_VEHICLE: { + float x = data->x; + float y = data->y; + float const w = (float)(data->veh_kind == 0 ? 80 : data->veh_kind == 1 ? 120 : 135); + float const h = 50; + Color color = data->veh_kind == 0 ? RED : data->veh_kind == 1 ? GREEN : BLUE; + DrawRectanglePro((Rectangle){x,y,w,h}, (Vector2){w/2.0f,h/2.0f}, zpl_to_degrees(data->heading), ColorAlpha(color, data->tran_time)); + }break; + case EKIND_DEVICE:{ + float x = data->x - 32.f; + float y = data->y - 32.f; + DrawTexturePro(GetSpriteTexture2D(assets_find(data->asset)), ASSET_SRC_RECT(), ASSET_DST_RECT(x,y), (Vector2){0.5f,0.5f}, 0.0f, ALPHA(WHITE)); + + if (data->progress_active) { + float w = 64.f; + float h = 8.f; + float p = data->progress_value; + float x = data->x - w/2.f; + float y = data->y - 32.f - h; + DrawRectangleEco(x, y, w, h, ColorAlpha(BLACK, data->tran_time)); + DrawRectangleEco(x, y, w*p, h, ColorAlpha(GREEN, data->tran_time)); + } + }break; + case EKIND_DEMO_NPC: { + float x = data->x; + float y = data->y; + DrawNametag("Demo", key, data, x, y); + DrawCircleEco(x, y, size, ColorAlpha(BLUE, data->tran_time)); + }break; + case EKIND_MONSTER: { + float x = data->x; + float y = data->y; + //DrawCircleEco(x, y, size, ColorAlpha(PINK, data->tran_time)); + DrawTextureRec(GetSpriteTexture2D(assets_find(ASSET_MOB)), ASSET_SRC_RECT(), (Vector2){data->x-(WORLD_BLOCK_SIZE/2), data->y-(WORLD_BLOCK_SIZE/2)}, ColorAlpha(WHITE, data->tran_time)); + float health = (data->hp / data->max_hp); + + if (health < 1.0f) { + DrawRectangleEco(x-32, y-48, 64, 8, ColorAlpha(BLACK, data->tran_time)); + DrawRectangleEco(x-32, y-48, 64*health, 8, ColorAlpha(RED, data->tran_time)); + } + }break; + case EKIND_PLAYER: { + float x = data->x; + float y = data->y; + float health = (data->hp / data->max_hp); + DrawNametag("Player", key, data, x, y); + DrawCircleEco(x, y, size, ColorAlpha(YELLOW, data->tran_time)); + + if (data->has_items && !data->inside_vehicle) { + float ix = data->x; + float iy = data->y; + if (data->items[data->selected_item].quantity > 0) { + asset_id it_kind = data->items[data->selected_item].kind; + uint32_t qty = data->items[data->selected_item].quantity; + DrawTexturePro(GetSpriteTexture2D(assets_find(it_kind)), ASSET_SRC_RECT(), ((Rectangle){ix, iy, 32, 32}), (Vector2){0.5f,0.5f}, 0.0f, ALPHA(WHITE)); + } + } + }break; + case EKIND_ITEM: { + float x = data->x - 32.f; + float y = data->y - 32.f; + DrawTexturePro(GetSpriteTexture2D(assets_find(data->asset)), ASSET_SRC_RECT(), ASSET_DST_RECT(x,y), (Vector2){0.5f,0.5f}, 0.0f, ALPHA(WHITE)); + + if (data->quantity > 1) { + DrawTextEco(zpl_bprintf("%d", data->quantity), x, y, 10, ALPHA(RAYWHITE), 0.0f); + } + + if (data->durability < 1.0f) { + DrawRectangleEco(x, y+32, 4, 32, BlendColor(RED, GREEN, data->durability)); + DrawRectangleEco(x, y+32, 4, 32*(1.0f-data->durability), ColorAlpha(BLACK, data->tran_time)); + } + }break; + default:break; + } +} + +void renderer_draw(void) { + render_camera.offset = (Vector2){(float)(screenWidth >> 1), (float)(screenHeight >> 1)}; + render_camera.zoom = zpl_lerp(render_camera.zoom, target_zoom, GetFrameTime()*2.9978f); + camera_update(); + + camera game_camera = camera_get(); + render_camera.target = (Vector2){(float)game_camera.x, (float)game_camera.y}; + + ClearBackground(GetColor(0x222034)); + BeginMode2D(render_camera); + + game_world_view_render_world(); + + +#if 0 + if (game_get_kind() == GAMEKIND_SINGLE) { + extern frWorld *phys_world; + frDrawSpatialHash(frGetWorldSpatialHash(phys_world), 0.75f, GRAY); + } +#endif + + + EndMode2D(); +} + +float renderer_zoom_get(void) { + return render_camera.zoom; +} + +void renderer_init(void) { + render_camera.target = (Vector2){0.0f,0.0f}; + render_camera.offset = (Vector2){(float)(screenWidth >> 1), (float)(screenHeight >> 1)}; + render_camera.rotation = 0.0f; + render_camera.zoom = 2.9f; + + // NOTE(zaklaus): Paint the screen before we load the game + // TODO(zaklaus): Render a cool loading screen background maybe? :wink: :wink: + + BeginDrawing(); + ClearBackground(GetColor(0x222034)); + + char const *loading_text = "zpl.eco2d is loading..."; + int text_w = MeasureText(loading_text, 120); + DrawText(loading_text, GetScreenWidth()-text_w-15, GetScreenHeight()-135, 120, RAYWHITE); + EndDrawing(); + + blocks_setup(); + assets_setup(); +} + +void renderer_shutdown(void) { + blocks_destroy(); + assets_destroy(); +} + +void renderer_debug_draw(void) { + BeginMode2D(render_camera); + debug_draw_queue *que = debug_draw_samples(); + + for (size_t i = 0; i < que->num_entries; i += 1) { + debug_draw_entry *e = &que->entries[i]; + Color color = GetColor(e->color); + + switch (e->kind) { + case DDRAW_LINE: { + float x = e->a.x; + float y = e->a.y; + float x2 = e->b.x; + float y2 = e->b.y; + DrawLineV((Vector2){x, y}, (Vector2){x2, y2}, color); + }break; + + case DDRAW_CIRCLE:{ + float x = e->a.x; + float y = e->a.y; + DrawCircleLinesEco(x, y, e->radius, color); + }break; + + case DDRAW_RECT:{ + float x = e->bmin.x; + float y = e->bmin.y; + float w = e->bmax.x - e->bmin.x; + float h = e->bmax.y - e->bmin.y; + DrawRectangleLinesEco(x, y, w, h, color); + }break; + + default: { + + }break; + } + } + + EndMode2D(); +} + +void renderer_draw_single(float x, float y, asset_id id, Color color) { + BeginMode2D(render_camera); + + x -= 32.0f; + y -= 32.0f; + + DrawTexturePro(GetSpriteTexture2D(assets_find(id)), ASSET_SRC_RECT(), ASSET_DST_RECT(x,y), (Vector2){0.5f,0.5f}, 0.0f, color); + + EndMode2D(); +} diff --git a/code/games/survival/src/rules.c b/code/games/survival/src/rules.c new file mode 100644 index 0000000..263d8bb --- /dev/null +++ b/code/games/survival/src/rules.c @@ -0,0 +1,5 @@ +#include "core/rules.h" + +void rules_setup() { + +} diff --git a/code/games/survival/src/texgen.c b/code/games/survival/src/texgen.c new file mode 100644 index 0000000..d3483b8 --- /dev/null +++ b/code/games/survival/src/texgen.c @@ -0,0 +1,17 @@ +#include "gen/texgen.h" +#include "world/world.h" +#include "zpl.h" +#include "utils/raylib_helpers.h" + +Texture2D texgen_build_anim(asset_id id, int64_t counter) { + (void)counter; + switch (id) { + default: return texgen_build_anim_fallback(id, counter); break; + } +} + +Texture2D texgen_build_sprite(asset_id id) { + switch (id) { + default: return texgen_build_sprite_fallback(id); break; + } +} diff --git a/code/games/survival/src/worldgen.c b/code/games/survival/src/worldgen.c new file mode 100644 index 0000000..83ab5ad --- /dev/null +++ b/code/games/survival/src/worldgen.c @@ -0,0 +1,69 @@ +#include "zpl.h" + +#include +#include + +#include "world/world.h" +#include "world/blocks.h" +#include "world/perlin.h" + +#include "models/components.h" +#include "models/entity.h" +#include "models/prefabs/vehicle.h" +#include "models/items.h" +#include "world/blocks_info.h" + +#include "world/worldgen_utils.h" + +block_id worldgen_biome_find(uint32_t biome, uint32_t kind) { + asset_id asset = ASSET_INVALID; + switch (biome) { + case BLOCK_BIOME_DEV: { + switch (kind) { + case BLOCK_KIND_GROUND: asset = ASSET_GROUND; break; + case BLOCK_KIND_DIRT: asset = ASSET_DIRT; break; + case BLOCK_KIND_WALL: asset = ASSET_WALL; break; + case BLOCK_KIND_HILL_SNOW: + case BLOCK_KIND_HILL: asset = ASSET_HILL; break; + case BLOCK_KIND_WATER: asset = ASSET_WATER; break; + case BLOCK_KIND_LAVA: asset = ASSET_LAVA; break; + } + } + } + + return blocks_find(asset); +} + +int32_t worldgen_build(world_data *wld) { + // TODO(zaklaus): pass world as an arg instead + world = wld; + + // TODO: perform world gen + // atm, we will fill the world with ground and surround it by walls + block_id wall_id = worldgen_biome_find(BLOCK_BIOME_DEV, BLOCK_KIND_WALL); + block_id grnd_id = worldgen_biome_find(BLOCK_BIOME_DEV, BLOCK_KIND_GROUND); + block_id dirt_id = worldgen_biome_find(BLOCK_BIOME_DEV, BLOCK_KIND_DIRT); + block_id watr_id = worldgen_biome_find(BLOCK_BIOME_DEV, BLOCK_KIND_WATER); + block_id lava_id = worldgen_biome_find(BLOCK_BIOME_DEV, BLOCK_KIND_LAVA); + block_id tree_id = blocks_find(ASSET_TREE); + + srand(world->seed); + + // walls + world_fill_rect(world->data, wall_id, 0, 0, world->dim, world->dim, NULL); + + // ground + world_fill_rect(world->data, wall_id, 1, 1, world->dim-2, world->dim-2, NULL); + + int radius = 30; + + // wide boy circle + world_fill_circle(world->data, dirt_id, world->dim / 2, world->dim / 2, radius, NULL); + + // narrow boy cirlce + world_fill_circle(world->data, grnd_id, world->dim / 2, world->dim / 2, (uint32_t)(radius * 0.7f), NULL); + + world_fill_circle(world->data, wall_id, world->dim / 2 + 9, world->dim / 2+ 9, (uint32_t)(radius * 0.2f), NULL); + + return WORLD_ERROR_NONE; +} diff --git a/code/vendors/CMakeLists.txt b/code/vendors/CMakeLists.txt index 7287ac4..e57ab8c 100644 --- a/code/vendors/CMakeLists.txt +++ b/code/vendors/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(flecs) add_subdirectory(cwpack) add_subdirectory(raylib-nuklear) +add_subdirectory(ferox) add_library(vendors-bundle STATIC sfd.c diff --git a/code/vendors/ferox/CMakeLists.txt b/code/vendors/ferox/CMakeLists.txt new file mode 100644 index 0000000..fd3ae3c --- /dev/null +++ b/code/vendors/ferox/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(ferox STATIC + src/broadphase.c + src/collision.c + src/debug.c + src/dynamics.c + src/geometry.c + src/timer.c + src/utils.c + src/world.c +) + +target_include_directories(ferox PUBLIC include) +target_include_directories(ferox PRIVATE src/external) + +target_link_libraries(ferox PRIVATE raylib) \ No newline at end of file diff --git a/code/vendors/ferox/include/ferox.h b/code/vendors/ferox/include/ferox.h new file mode 100644 index 0000000..0b1b20a --- /dev/null +++ b/code/vendors/ferox/include/ferox.h @@ -0,0 +1,746 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#ifndef FEROX_H +#define FEROX_H + +#include +#include +#include +#include +#include + +#ifndef FEROX_STANDALONE + #include "raylib.h" +#else + #ifndef PI + #define PI 3.14159265358979323846f + #endif + + #define DEG2RAD (PI / 180.0f) + #define RAD2DEG (180.0f / PI) + + /* A struct that represents a two-dimensional vector. */ + typedef struct Vector2 { + float x; + float y; + } Vector2; + + /* A struct that represents a rectangle. */ + typedef struct Rectangle { + float x; + float y; + float width; + float height; + } Rectangle; + +#ifdef __cplusplus +extern "C" { +#endif + /* Returns `true` if `rec1` collides with `rec2`. */ + bool CheckCollisionRecs(Rectangle rec1, Rectangle rec2) { + return ((rec1.x + rec1.width) - rec2.x) >= 0 && ((rec2.x + rec2.width) - rec1.x) >= 0 + && ((rec1.y + rec1.height) - rec2.y) >= 0 && ((rec2.y + rec2.height) - rec1.y) >= 0; + } +#ifdef __cplusplus +} +#endif + +#endif + +/* | Macro Definitions (Global) | */ + +#ifdef _MSC_VER + #define FR_API_INLINE __forceinline +#elif defined(__GNUC__) + #if defined(__STRICT_ANSI__) + #define FR_API_INLINE __inline__ __attribute__((always_inline)) + #else + #define FR_API_INLINE inline __attribute__((always_inline)) + #endif +#else + #define FR_API_INLINE inline +#endif + +#define FR_STRUCT_ZERO(T) ((T) { 0 }) + +/* | Macro Definitions (Configuration) | */ + +#define FR_BROADPHASE_CELL_SIZE 3.2f +#define FR_BROADPHASE_INVERSE_CELL_SIZE (1.0f / (FR_BROADPHASE_CELL_SIZE)) + +#define FR_DEBUG_CIRCLE_SEGMENT_COUNT 32 + +#define FR_DYNAMICS_CORRECTION_DEPTH_SCALE 0.24f +#define FR_DYNAMICS_CORRECTION_DEPTH_THRESHOLD 0.02f +#define FR_DYNAMICS_DYNAMIC_FRICTION_MULTIPLIER 0.85f + +#define FR_GEOMETRY_MAX_VERTEX_COUNT 8 + +#define FR_GLOBAL_PIXELS_PER_METER 16.0f + +#define FR_WORLD_ACCUMULATOR_LIMIT 200.0 +#define FR_WORLD_DEFAULT_GRAVITY ((Vector2) { .y = 9.8f }) +#define FR_WORLD_MAX_BODY_COUNT 288 +#define FR_WORLD_MAX_ITERATIONS 12 + +/* | Data Type Definitions | */ + +/* A struct that represents the type of a rigid body. */ +typedef enum frBodyType { + FR_BODY_UNKNOWN = -1, + FR_BODY_STATIC, + FR_BODY_KINEMATIC, + FR_BODY_DYNAMIC +} frBodyType; + +/* An enum that represents the property flag of a rigid body. */ +typedef enum frBodyFlag { + FR_FLAG_NONE = 0x00, + FR_FLAG_INFINITE_MASS = 0x01, + FR_FLAG_INFINITE_INERTIA = 0x02 +} frBodyFlag; + +/* A data type that represents the property flags of a rigid body. */ +typedef uint8_t frBodyFlags; + +/* A struct that represents the type of a collision shape. */ +typedef enum frShapeType { + FR_SHAPE_UNKNOWN, + FR_SHAPE_CIRCLE, + FR_SHAPE_POLYGON +} frShapeType; + +/* A struct that represents the material of a collision shape. */ +typedef struct frMaterial { + float density; + float restitution; + float staticFriction; + float dynamicFriction; +} frMaterial; + +/* + A struct that represents the position (in meters) + and rotation (in radians) of an object. +*/ +typedef struct frTransform { + Vector2 position; + float rotation; + struct { + bool valid; + float sinA; + float cosA; + } cache; +} frTransform; + +/* A struct that represents the vertices of a polygon. */ +typedef struct frVertices { + Vector2 data[FR_GEOMETRY_MAX_VERTEX_COUNT]; + int count; +} frVertices; + +/* A struct that represents a collision shape. */ +typedef struct frShape frShape; + +/* A struct that represents a rigid body. */ +typedef struct frBody frBody; + +/* A struct that represents information for a set of two rigid bodies. */ +typedef struct frSolverCache { + frBody *bodies[2]; + /* TODO: ... */ +} frSolverCache; + +/* A struct that represents the details of a collision. */ +typedef struct frCollision { + bool check; // Returns `true` if two collision shapes collide with each other. + frSolverCache cache; // The struct that contains rigid bodies that collided with each other. + Vector2 direction; // The direction of the collision in a unit vector form. + Vector2 points[2]; // The points of the collision between two collision shapes. + float depths[2]; // The penetration depths of the collision. + int count; // The number of points for this collision. +} frCollision; + +/* A callback which will be executed when a collision event occurs. */ +typedef void (*frCollisionCallback)(frCollision *collision); + +/* A struct that represents the collision event handler of a world. */ +typedef struct frCollisionHandler { + frCollisionCallback preSolve; + frCollisionCallback postSolve; +} frCollisionHandler; + +/* A struct that represents a ray. */ +typedef struct frRay { + Vector2 origin; + Vector2 direction; + float maxDistance; + bool closest; +} frRay; + +/* A struct that represents the details of a raycast hit. */ +typedef struct frRaycastHit { + bool check; // Returns `true` if the ray collides with `shape` or `body`. + union { + frShape *shape; + frBody *body; + }; // The collision shape or the body that was hit by the raycast. + Vector2 point; // The point at which the raycast hit `shape` or `body`. + Vector2 normal; // The normal vector of the raycast hit. + float distance; // The distance from the ray's starting point to `shape` or `body`. + bool inside; // Returns `true` if the ray's starting point is inside `shape` or `body`. +} frRaycastHit; + +/* A struct that represents a spatial hash. */ +typedef struct frSpatialHash frSpatialHash; + +/* A struct that represents the world that holds rigid bodies. */ +typedef struct frWorld frWorld; + +#ifdef __cplusplus +extern "C" { +#endif + +/* | Module Functions (`broadphase`) | */ + +/* Creates a new spatial hash from the given bounds and the size of each cell. */ +frSpatialHash *frCreateSpatialHash(Rectangle bounds, float cellSize); + +/* Releases the memory allocated for `hash`. */ +void frReleaseSpatialHash(frSpatialHash *hash); + +/* Generates a new key from `rec` and inserts the key-`value` pair to `hash`. */ +void frAddToSpatialHash(frSpatialHash *hash, Rectangle rec, int value); + +/* Removes all key-value pairs from `hash`. */ +void frClearSpatialHash(frSpatialHash *hash); + +/* Removes a `key`-value pair from `hash`. */ +void frRemoveFromSpatialHash(frSpatialHash *hash, int key); + +/* Returns the values of all pairs that collides with `rec` in `hash`. */ +void frQuerySpatialHash(frSpatialHash *hash, Rectangle rec, int **result); + +/* Returns the bounds of `hash`. */ +Rectangle frGetSpatialHashBounds(frSpatialHash *hash); + +/* Returns the size of each cell of `hash`. */ +float frGetSpatialHashCellSize(frSpatialHash *hash); + +/* Sets the bounds of `hash` to `bounds`. */ +void frSetSpatialHashBounds(frSpatialHash *hash, Rectangle bounds); + +/* Sets the size of each cell of `hash` to `cellSize`. */ +void frSetSpatialHashCellSize(frSpatialHash *hash, float cellSize); + +/* | Module Functions (`collision`) | */ + +/* + Computes the collision between `s1` with position and rotation from `tx1` and `s2` + with position and rotation from `tx2`. +*/ +frCollision frComputeShapeCollision(frShape *s1, frTransform tx1, frShape *s2, frTransform tx2); + +/* Computes the collision between `b1` and `b2`. */ +frCollision frComputeBodyCollision(frBody *b1, frBody *b2); + +/* Casts a `ray` to the collision shape `s`. */ +frRaycastHit frComputeShapeRaycast(frShape *s, frTransform tx, frRay ray); + +/* Casts a `ray` to the rigid body `b`. */ +frRaycastHit frComputeBodyRaycast(frBody *b, frRay ray); + +/* | Module Functions (`debug`) | */ + +#ifndef FEROX_STANDALONE + /* Draws an arrow that points from `p1` to `p2`. */ + void frDrawArrow(Vector2 p1, Vector2 p2, float thick, Color color); + + /* Draws the collision shape of `b`. */ + void frDrawBody(frBody *b, Color color); + + /* Draws the border of the collision shape of `b`. */ + void frDrawBodyLines(frBody *b, float thick, Color color); + + /* Draws the AABB and the center of mass of `b`. */ + void frDrawBodyAABB(frBody *b, float thick, Color color); + + /* Draws the properties of `b`. */ + void frDrawBodyProperties(frBody *b, Color color); + + /* Draws the border of `hm`. */ + void frDrawSpatialHash(frSpatialHash *hm, float thick, Color color); +#endif + +/* | Module Functions (`dynamics`) | */ + +/* Creates a new rigid body from the given type, flags, and position (in meters). */ +frBody *frCreateBody(frBodyType type, frBodyFlags flags, Vector2 p); + +/* + Creates a new rigid body from the given type, flags, position (in meters), + and a collision shape. +*/ +frBody *frCreateBodyFromShape(frBodyType type, frBodyFlags flags, Vector2 p, frShape *s); + +/* Releases the memory allocated for `b`. */ +void frReleaseBody(frBody *b); + +/* Attaches the collision shape `s` to `b`. */ +void frAttachShapeToBody(frBody *b, frShape *s); + +/* Detaches the collision shape `s` from `b`. */ +void frDetachShapeFromBody(frBody *b); + +/* Returns the size of the struct `frBody`. */ +size_t frGetBodyStructSize(void); + +/* Returns the type of `b`. */ +frBodyType frGetBodyType(frBody *b); + +/* Returns the property flags of `b`. */ +frBodyFlags frGetBodyFlags(frBody *b); + +/* Returns the material of `b`. */ +frMaterial frGetBodyMaterial(frBody *b); + +/* Returns the mass of `b`. */ +float frGetBodyMass(frBody *b); + +/* Returns the inverse mass of `b`. */ +float frGetBodyInverseMass(frBody *b); + +/* Returns the moment of inertia for `b`. */ +float frGetBodyInertia(frBody *b); + +/* Returns the inverse moment of inertia for `b`. */ +float frGetBodyInverseInertia(frBody *b); + +/* Returns the velocity of `b`. */ +Vector2 frGetBodyVelocity(frBody *b); + +/* Returns the angular velocity of `b`. */ +float frGetBodyAngularVelocity(frBody *b); + +/* Returns the gravity scale of `b`. */ +float frGetBodyGravityScale(frBody *b); + +/* Returns the position (in meters) and rotation (in radians) of `b`. */ +frTransform frGetBodyTransform(frBody *b); + +/* Returns the position (in meters) of `b`. */ +Vector2 frGetBodyPosition(frBody *b); + +/* Returns the rotation (in radians) of `b`. */ +float frGetBodyRotation(frBody *b); + +/* Returns the collision shape of `b`. */ +frShape *frGetBodyShape(frBody *b); + +/* Returns the AABB (Axis-Aligned Bounding Box) of `b`. */ +Rectangle frGetBodyAABB(frBody *b); + +/* Returns the user data of `b`. */ +void *frGetBodyUserData(frBody *b); + +/* Converts the world coordinates `p` to a position relative to `b`'s transform value. */ +Vector2 frGetLocalPoint(frBody *b, Vector2 p); + +/* Converts the position relative to `b`'s transform value `p` to world coordinates. */ +Vector2 frGetWorldPoint(frBody *b, Vector2 p); + +/* Sets the type of `b` to `type`. */ +void frSetBodyType(frBody *b, frBodyType type); + +/* Sets the property flags of `b` to `flags`. */ +void frSetBodyFlags(frBody *b, frBodyFlags flags); + +/* Sets the velocity of `b` to `v`. */ +void frSetBodyVelocity(frBody *b, Vector2 v); + +/* Sets the angular velocity of `b` to `a`. */ +void frSetBodyAngularVelocity(frBody *b, double a); + +/* Sets the gravity scale value of `b` to `scale`. */ +void frSetBodyGravityScale(frBody *b, float scale); + +/* Sets the transform value of `b` to `tx`. */ +void frSetBodyTransform(frBody *b, frTransform tx); + +/* Sets the position of `b` to `p`. */ +void frSetBodyPosition(frBody *b, Vector2 p); + +/* Sets the rotation of `b` to `rotation`. */ +void frSetBodyRotation(frBody *b, float rotation); + +/* Sets the user data of `b` to `data`. */ +void frSetBodyUserData(frBody *b, void *data); + +/* Clears all forces that are applied to `b`. */ +void frClearBodyForces(frBody *b); + +/* Applies a `gravity` vector to `b`. */ +void frApplyGravity(frBody *b, Vector2 gravity); + +/* Applies an `impulse` to `b`. */ +void frApplyImpulse(frBody *b, Vector2 impulse); + +/* Applies a torque `impulse` to `b`. */ +void frApplyTorqueImpulse(frBody *b, Vector2 p, Vector2 impulse); + +/* Integrates the position of `b` with `dt`. */ +void frIntegrateForBodyPosition(frBody *b, double dt); + +/* Integrates the velocities of `b` with `dt`. */ +void frIntegrateForBodyVelocities(frBody *b, double dt); + +/* Resolves the collision between two rigid bodies. */ +void frResolveCollision(frCollision *collision); + +/* Corrects the positions of two rigid bodies. */ +void frCorrectBodyPositions(frCollision *collision, float inverseDt); + +/* | Module Functions (`geometry`) | */ + +/* + Creates a new circle-shaped collision shape + from the given material and radius (in meters). +*/ +frShape *frCreateCircle(frMaterial material, float radius); + +/* + Creates a new rectangle-shaped collision shape + from the given material, width and height (in meters). +*/ +frShape *frCreateRectangle(frMaterial material, float width, float height); + +/* + Creates a new polygon-shaped collision shape + from the given material and vertices (in meters). +*/ +frShape *frCreatePolygon(frMaterial material, frVertices vertices); + +/* Creates an empty shape. */ +frShape *frCreateShape(void); + +/* Returns a clone of `s`. */ +frShape *frCloneShape(frShape *s); + +/* Releases the memory allocated for `s`. */ +void frReleaseShape(frShape *s); + +/* Returns the size of the struct `frShape`. */ +size_t frGetShapeStructSize(void); + +/* Returns the type of `s`. */ +frShapeType frGetShapeType(frShape *s); + +/* Returns the material of `s`. */ +frMaterial frGetShapeMaterial(frShape *s); + +/* Returns the area of `s`. */ +float frGetShapeArea(frShape *s); + +/* Returns the mass of `s`. */ +float frGetShapeMass(frShape *s); + +/* Returns the moment of inertia for `s`. */ +float frGetShapeInertia(frShape *s); + +/* Returns the AABB (Axis-Aligned Bounding Box) of `s`. */ +Rectangle frGetShapeAABB(frShape *s, frTransform tx); + +/* Returns the radius of the `s`. */ +float frGetCircleRadius(frShape *s); + +/* Returns the width and height of `s`. */ +Vector2 frGetRectangleDimensions(frShape *s); + +/* Returns the `i + 1`th vertex of `s`. */ +Vector2 frGetPolygonVertex(frShape *s, int i); + +/* Returns the `i + 1`th normal of `s`. */ +Vector2 frGetPolygonNormal(frShape *s, int i); + +/* Returns the vertices of `s`. */ +frVertices frGetPolygonVertices(frShape *s); + +/* Returns the normals of `s`. */ +frVertices frGetPolygonNormals(frShape *s); + +/* Returns `true` if `s` is a rectangular collision shape. */ +bool frIsShapeRectangle(frShape *s); + +/* Sets the radius of `s` to `radius`. */ +void frSetCircleRadius(frShape *s, float radius); + +/* Sets the dimensions (width and height) for `s` to `v`. */ +void frSetRectangleDimensions(frShape *s, Vector2 v); + +/* Sets the vertices of `s` to `vertices`. */ +void frSetPolygonVertices(frShape *s, frVertices vertices); + +/* Sets the material of `s` to `material`. */ +void frSetShapeMaterial(frShape *s, frMaterial material); + +/* Sets the type of `s` to `type`. */ +void frSetShapeType(frShape *s, frShapeType type); + +/* Returns `true` if `s` contains the point `p`. */ +bool frShapeContainsPoint(frShape *s, frTransform tx, Vector2 p); + +/* | Module Functions (`timer`) | */ + +/* Initializes a monotonic clock. */ +void frInitClock(void); + +/* Returns the current time (in milliseconds) of the monotonic clock. */ +double frGetCurrentTime(void); + +/* Returns the difference between `newTime` and `oldTime`. */ +double frGetTimeDifference(double newTime, double oldTime); + +/* Returns the difference between the current time and `oldTime`. */ +double frGetTimeSince(double oldTime); + +/* | Module Functions (`utils`) | */ + +/* Normalizes `angle` (in radians) to range `[center - π/2, center + π/2]`. */ +float frNormalizeAngle(float angle, float center); + +/* Returns `true` if `f1` and `f2` are approximately equal. */ +bool frNumberApproxEquals(float f1, float f2); + +/* | Module Functions (`world`) | */ + +/* Creates a new world from the given `gravity` vector and world `bounds` in meters. */ +frWorld *frCreateWorld(Vector2 gravity, Rectangle bounds); + +/* Removes all rigid bodies from `world`, then releases the memory allocated for `world`. */ +void frReleaseWorld(frWorld *world); + +/* Adds `b` to `world`. */ +bool frAddToWorld(frWorld *world, frBody *b); + +/* Removes all rigid bodies from `world`. */ +void frClearWorld(frWorld *world); + +/* Removes `b` from `world`. */ +bool frRemoveFromWorld(frWorld *world, frBody *b); + +/* Returns the size of the struct `frWorld`. */ +size_t frGetWorldStructSize(void); + +/* Returns the rigid body at the `index` in `world`'s array of rigid bodies. */ +frBody *frGetWorldBody(frWorld *world, int index); + +/* Returns the collision event handler for `world`. */ +frCollisionHandler frGetWorldCollisionHandler(frWorld *world); + +/* Returns the length of `world`'s array of rigid bodies. */ +int frGetWorldBodyCount(frWorld *world); + +/* Returns the bounds of `world`. */ +Rectangle frGetWorldBounds(frWorld *world); + +/* Returns the spatial hash of `world`. */ +frSpatialHash *frGetWorldSpatialHash(frWorld *world); + +/* Returns the gravity vector of `world`. */ +Vector2 frGetWorldGravity(frWorld *world); + +/* Returns `true` if `b` collides with the bounds of `world`. */ +bool frIsInWorldBounds(frWorld *world, frBody *b); + +/* Sets the world bounds of `world` to `bounds`. */ +void frSetWorldBounds(frWorld *world, Rectangle bounds); + +/* Sets the collision event handler for `world` to `handler`. */ +void frSetWorldCollisionHandler(frWorld *world, frCollisionHandler handler); + +/* Sets the gravity vector of `world` to `gravity`. */ +void frSetWorldGravity(frWorld *world, Vector2 gravity); + +/* Simulates the `world` for the time step `dt` (in milliseconds). */ +void frSimulateWorld(frWorld *world, double dt); + +/* Query the `world` for rigid bodies that collides with `rec`. */ +int frQueryWorldSpatialHash(frWorld *world, Rectangle rec, frBody **bodies); + +/* Casts a `ray` to all rigid bodies in `world`. */ +int frComputeWorldRaycast(frWorld *world, frRay ray, frRaycastHit *hits); + +/* | Inline Functions | */ + +/* Converts `value` (in pixels) to meters. */ +FR_API_INLINE float frNumberPixelsToMeters(float value) { + return (FR_GLOBAL_PIXELS_PER_METER > 0.0f) + ? (value / FR_GLOBAL_PIXELS_PER_METER) + : 0.0f; +} + +/* Converts `value` (in meters) to pixels. */ +FR_API_INLINE float frNumberMetersToPixels(float value) { + return (FR_GLOBAL_PIXELS_PER_METER > 0.0f) + ? (value * FR_GLOBAL_PIXELS_PER_METER) + : 0.0f; +} + +/* Converts the components of `rec` (in pixels) to meters. */ +FR_API_INLINE Rectangle frRecPixelsToMeters(Rectangle rec) { + return (Rectangle) { + .x = frNumberPixelsToMeters(rec.x), + .y = frNumberPixelsToMeters(rec.y), + .width = frNumberPixelsToMeters(rec.width), + .height = frNumberPixelsToMeters(rec.height) + }; +} + +/* Converts the components of `rec` (in meters) to pixels. */ +FR_API_INLINE Rectangle frRecMetersToPixels(Rectangle rec) { + return (Rectangle) { + .x = frNumberMetersToPixels(rec.x), + .y = frNumberMetersToPixels(rec.y), + .width = frNumberMetersToPixels(rec.width), + .height = frNumberMetersToPixels(rec.height) + }; +} + +/* Adds `v1` and `v2`. */ +FR_API_INLINE Vector2 frVec2Add(Vector2 v1, Vector2 v2) { + return (Vector2) { v1.x + v2.x, v1.y + v2.y }; +} + +/* Subtracts `v2` from `v1`. */ +FR_API_INLINE Vector2 frVec2Subtract(Vector2 v1, Vector2 v2) { + return (Vector2) { v1.x - v2.x, v1.y - v2.y }; +} + +/* Multiplies `v` by `value`. */ +FR_API_INLINE Vector2 frVec2ScalarMultiply(Vector2 v, float value) { + return (Vector2) { v.x * value, v.y * value }; +} + +/* Returns the cross product of `v1` and `v2`. */ +FR_API_INLINE float frVec2CrossProduct(Vector2 v1, Vector2 v2) { + // 평면 벡터의 외적은 스칼라 값이다. + return (v1.x * v2.y) - (v1.y * v2.x); +} + +/* Returns the dot product of `v1` and `v2`. */ +FR_API_INLINE float frVec2DotProduct(Vector2 v1, Vector2 v2) { + return (v1.x * v2.x) + (v1.y * v2.y); +} + +/* Returns the squared magnitude of `v`. */ +FR_API_INLINE float frVec2MagnitudeSqr(Vector2 v) { + return frVec2DotProduct(v, v); +} + +/* Returns the magnitude of `v`. */ +FR_API_INLINE float frVec2Magnitude(Vector2 v) { + return sqrtf(frVec2MagnitudeSqr(v)); +} + +/* Returns the negated vector of `v`. */ +FR_API_INLINE Vector2 frVec2Negate(Vector2 v) { + return (Vector2) { -v.x, -v.y }; +} + +/* Converts `v` to a unit vector. */ +FR_API_INLINE Vector2 frVec2Normalize(Vector2 v) { + const float magnitude = frVec2Magnitude(v); + + return (magnitude > 0.0f) + ? frVec2ScalarMultiply(v, 1.0f / magnitude) + : FR_STRUCT_ZERO(Vector2); +} + +/* Returns the angle (in radians) between `v1` and `v2`. */ +FR_API_INLINE float frVec2Angle(Vector2 v1, Vector2 v2) { + return atan2f(v2.y, v2.x) - atan2f(v1.y, v1.x); +} + +/* Returns `true` if `v1` and `v2` are approximately equal. */ +FR_API_INLINE bool frVec2ApproxEquals(Vector2 v1, Vector2 v2) { + return frNumberApproxEquals(v1.x, v2.x) && frNumberApproxEquals(v1.y, v2.y); +} + +/* Returns the left normal of `v`. */ +FR_API_INLINE Vector2 frVec2LeftNormal(Vector2 v) { + return frVec2Normalize((Vector2) { -v.y, v.x }); +} + +/* Returns the right normal of `v`. */ +FR_API_INLINE Vector2 frVec2RightNormal(Vector2 v) { + return frVec2Normalize((Vector2) { v.y, -v.x }); +} + +/* Rotates `v` around the origin. */ +FR_API_INLINE Vector2 frVec2Rotate(Vector2 v, float angle) { + const float sinA = sinf(angle); + const float cosA = cosf(angle); + + return (Vector2) { (v.x * cosA - v.y * sinA), (v.x * sinA + v.y * cosA) }; +} + +/* Rotates `v` with the properties of `tx`. */ +FR_API_INLINE Vector2 frVec2RotateTx(Vector2 v, frTransform tx) { + Vector2 result = { + (v.x * tx.cache.cosA - v.y * tx.cache.sinA), + (v.x * tx.cache.sinA + v.y * tx.cache.cosA) + }; + + if (!tx.cache.valid) result = frVec2Rotate(v, tx.rotation); + + return result; +} + +/* Transforms `v` with the properties of `tx`. */ +FR_API_INLINE Vector2 frVec2Transform(Vector2 v, frTransform tx) { + return frVec2Add(tx.position, frVec2RotateTx(v, tx)); +} + +/* Returns `true` if `v1`, `v2` and `v3` are ordered counter-clockwise. */ +FR_API_INLINE bool frVec2CCW(Vector2 v1, Vector2 v2, Vector2 v3) { + return (v3.y - v1.y) * (v2.x - v1.x) < (v2.y - v1.y) * (v3.x - v1.x); +} + +/* Converts the components of `v` (in pixels) to meters. */ +FR_API_INLINE Vector2 frVec2PixelsToMeters(Vector2 v) { + return (FR_GLOBAL_PIXELS_PER_METER > 0.0f) + ? frVec2ScalarMultiply(v, 1.0f / FR_GLOBAL_PIXELS_PER_METER) + : FR_STRUCT_ZERO(Vector2); +} + +/* Converts the components of `v` (in meters) to pixels. */ +FR_API_INLINE Vector2 frVec2MetersToPixels(Vector2 v) { + return (FR_GLOBAL_PIXELS_PER_METER > 0.0f) + ? frVec2ScalarMultiply(v, FR_GLOBAL_PIXELS_PER_METER) + : FR_STRUCT_ZERO(Vector2); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/code/vendors/ferox/src/broadphase.c b/code/vendors/ferox/src/broadphase.c new file mode 100644 index 0000000..b2858e0 --- /dev/null +++ b/code/vendors/ferox/src/broadphase.c @@ -0,0 +1,201 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" +#include "stb_ds.h" + +/* | `broadphase` 모듈 자료형 정의... | */ + +/* 공간 해시맵의 키와 값을 나타내는 구조체. */ +typedef struct frSpatialEntry { + int key; + int *values; +} frSpatialEntry; + +/* 공간 해시맵을 나타내는 구조체. */ +typedef struct frSpatialHash { + Rectangle bounds; + float cellSize; + float inverseCellSize; + frSpatialEntry *entries; + int *queryCache; +} frSpatialHash; + +/* | `broadphase` 모듈 함수... | */ + +/* 공간 해시맵 `hash`에서 벡터 `v`와 대응하는 키를 반환한다. */ +static FR_API_INLINE int frComputeSpatialHashKey(frSpatialHash *hash, Vector2 v); + +/* C 표준 라이브러리의 `qsort()` 함수 호출에 사용되는 비교 함수이다. */ +static int frQuickSortCallback(const void *x, const void *y); + +/* 경계 범위가 `bounds`이고 각 셀의 크기가 `cellSize`인 공간 해시맵의 메모리 주소를 반환한다. */ +frSpatialHash *frCreateSpatialHash(Rectangle bounds, float cellSize) { + if (cellSize <= 0.0f) return NULL; + + frSpatialHash *result = calloc(1, sizeof(*result)); + + result->bounds = bounds; + result->cellSize = cellSize; + result->inverseCellSize = 1.0f / cellSize; + + return result; +} + +/* 공간 해시맵 `hash`에 할당된 메모리를 해제한다. */ +void frReleaseSpatialHash(frSpatialHash *hash) { + if (hash == NULL) return; + + for (int i = 0; i < hmlen(hash->entries); i++) + arrfree(hash->entries[i].values); + + hmfree(hash->entries); + + free(hash); +} + +/* 공간 해시맵 `hash`에 직사각형 `rec`로 생성한 키와 `value`를 추가한다. */ +void frAddToSpatialHash(frSpatialHash *hash, Rectangle rec, int value) { + if (hash == NULL || !CheckCollisionRecs(hash->bounds, rec)) return; + + int x0 = frComputeSpatialHashKey(hash, (Vector2) { .x = rec.x }); + int x1 = frComputeSpatialHashKey(hash, (Vector2) { .x = rec.x + rec.width }); + + int y0 = frComputeSpatialHashKey(hash, (Vector2) { .y = rec.y }); + int y1 = frComputeSpatialHashKey(hash, (Vector2) { .y = rec.y + rec.height }); + + for (int y = y0; y <= y1; y += hash->bounds.width) { + for (int x = x0; x <= x1; x++) { + const int key = x + y; + + frSpatialEntry *entry = hmgetp_null(hash->entries, key); + + if (entry != NULL) { + arrput(entry->values, value); + } else { + frSpatialEntry newEntry = { .key = key }; + + arrput(newEntry.values, value); + hmputs(hash->entries, newEntry); + } + } + } +} + +/* 공간 해시맵 `hash`의 모든 키와 값을 제거한다. */ +void frClearSpatialHash(frSpatialHash *hash) { + if (hash == NULL) return; + + for (int i = 0; i < hmlen(hash->entries); i++) + arrsetlen(hash->entries[i].values, 0); +} + +/* 공간 해시맵 `hash`에서 키가 `key`인 값을 제거한다. */ +void frRemoveFromSpatialHash(frSpatialHash *hash, int key) { + if (hash == NULL) return; + + frSpatialEntry entry = hmgets(hash->entries, key); + arrfree(entry.values); + + hmdel(hash->entries, key); +} + +/* 공간 해시맵 `hash`에서 직사각형 `rec`와 경계 범위가 겹치는 모든 강체의 인덱스를 반환한다. */ +void frQuerySpatialHash(frSpatialHash *hash, Rectangle rec, int **result) { + if (hash == NULL || result == NULL || !CheckCollisionRecs(hash->bounds, rec)) return; + + int x0 = frComputeSpatialHashKey(hash, (Vector2) { .x = rec.x }); + int x1 = frComputeSpatialHashKey(hash, (Vector2) { .x = rec.x + rec.width }); + + int y0 = frComputeSpatialHashKey(hash, (Vector2) { .y = rec.y }); + int y1 = frComputeSpatialHashKey(hash, (Vector2) { .y = rec.y + rec.height }); + + const int yOffset = (int) hash->bounds.width; + + for (int y = y0; y <= y1; y += yOffset) { + for (int x = x0; x <= x1; x++) { + const int key = x + y; + + // 주어진 셀과 만나는 모든 강체의 인덱스를 구한다. + frSpatialEntry *entry = hmgetp_null(hash->entries, key); + + if (entry != NULL) { + for (int j = 0; j < arrlen(entry->values); j++) + arrput(*result, entry->values[j]); + } + } + } + + const size_t oldLength = arrlen(*result); + + if (oldLength <= 1) return; + + // 결과 배열을 먼저 퀵-정렬한다. + qsort(*result, oldLength, sizeof(**result), frQuickSortCallback); + + size_t newLength = 0; + + // 결과 배열에서 중복 값을 제거한다. + for (int i = 0; i < oldLength; i++) + if ((*result)[i] != (*result)[i + 1]) + (*result)[newLength++] = (*result)[i]; + + // 결과 배열의 길이를 다시 설정한다. + arrsetlen(*result, newLength); +} + +/* 공간 해시맵 `hash`의 경계 범위를 반환한다. */ +Rectangle frGetSpatialHashBounds(frSpatialHash *hash) { + return (hash != NULL) ? hash->bounds : FR_STRUCT_ZERO(Rectangle); +} + +/* 공간 해시맵 `hash`의 각 셀의 크기를 반환한다. */ +float frGetSpatialHashCellSize(frSpatialHash *hash) { + return (hash != NULL) ? hash->cellSize : 0.0f; +} + +/* 공간 해시맵 `hash`의 경계 범위를 `bounds`로 설정한다. */ +void frSetSpatialHashBounds(frSpatialHash *hash, Rectangle bounds) { + if (hash != NULL) hash->bounds = bounds; +} + +/* 공간 해시맵 `hash`의 각 셀의 크기를 `cellSize`로 설정한다. */ +void frSetSpatialHashCellSize(frSpatialHash *hash, float cellSize) { + if (hash != NULL) hash->cellSize = cellSize; +} + +/* 공간 해시맵 `hash`에서 벡터 `v`와 대응하는 키를 반환한다. */ +static FR_API_INLINE int frComputeSpatialHashKey(frSpatialHash *hash, Vector2 v) { + if (hash == NULL) return -1; + + const int xIndex = v.x * hash->inverseCellSize; + const int yIndex = v.y * hash->inverseCellSize; + + return yIndex * (int) hash->bounds.width + xIndex; +} + +/* C 표준 라이브러리의 `qsort()` 함수 호출에 사용되는 비교 함수이다. */ +static int frQuickSortCallback(const void *x, const void *y) { + int nx = *(const int *) x, ny = *(const int *) y; + + return (nx > ny) - (nx < ny); +} diff --git a/code/vendors/ferox/src/collision.c b/code/vendors/ferox/src/collision.c new file mode 100644 index 0000000..3af5bd5 --- /dev/null +++ b/code/vendors/ferox/src/collision.c @@ -0,0 +1,661 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +/* | `collision` 모듈 함수... | */ + +/* 도형 `s1`의 AABB가 `s2`의 AABB와 충돌하는지 확인한다. */ +static bool frCheckCollisionAABB(frShape *s1, frTransform tx1, frShape *s2, frTransform tx2); + +/* 다각형 `s`에서 벡터 `v`와의 내적이 가장 큰 꼭짓점의 인덱스를 반환한다. */ +static int frGetPolygonFurthestIndex(frShape *s, frTransform tx, Vector2 v); + +/* 도형 `s`에서 벡터 `v`와 가장 수직에 가까운 변을 반환한다. */ +static frVertices frGetShapeSignificantEdge(frShape *s, frTransform tx, Vector2 v); + +/* 다각형 `s1`에서 `s2`로의 충돌 방향과 깊이를 계산한다. */ +static int frGetSeparatingAxisIndex( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2, + float *depth +); + +/* 최적화된 분리 축 정리를 이용하여, 원 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionCirclesSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +); + +/* 최적화된 분리 축 정리를 이용하여, 원 `s1`에서 다각형 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionCirclePolySAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +); + +/* 최적화된 분리 축 정리를 이용하여, 다각형 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionPolysSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +); + +/* 최적화된 분리 축 정리를 이용하여, 도형 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +); + +/* 선분 `e`에서 벡터 `v`와의 내적이 `minDot`보다 작은 부분을 모두 잘라낸다. */ +static frVertices frClipEdgeWithAxis(frVertices e, Vector2 v, float minDot); + +/* `o1`에서 `v1` 방향으로 진행하는 광선이 `o2`에서 `v2` 방향으로 진행하는 광선과 만나는지 계산한다. */ +static bool frComputeIntersectionRays( + Vector2 o1, Vector2 v1, + Vector2 o2, Vector2 v2, + float *distance +); + +/* `o`에서 `v` 방향으로 진행하는 광선이 중심점이 `c`이고 반지름이 `r`인 원과 만나는지 계산한다. */ +static bool frComputeIntersectionRayCircle( + Vector2 o, Vector2 v, + Vector2 c, float r, + float *distance +); + +/* 도형 `s1`에서 `s2`로의 충돌을 계산한다. */ +frCollision frComputeShapeCollision(frShape *s1, frTransform tx1, frShape *s2, frTransform tx2) { + return frCheckCollisionAABB(s1, tx1, s2, tx2) + ? frComputeCollisionSAT(s1, tx1, s2, tx2) + : FR_STRUCT_ZERO(frCollision); +} + +/* 강체 `b1`에서 `b2`로의 충돌을 계산한다. */ +frCollision frComputeBodyCollision(frBody *b1, frBody *b2) { + if (b1 == NULL || b2 == NULL) return FR_STRUCT_ZERO(frCollision); + + return frComputeShapeCollision( + frGetBodyShape(b1), + frGetBodyTransform(b1), + frGetBodyShape(b2), + frGetBodyTransform(b2) + ); +} + +/* 도형 `s`에 광선을 투사한다. */ +frRaycastHit frComputeShapeRaycast(frShape *s, frTransform tx, frRay ray) { + if (s == NULL || frGetShapeType(s) == FR_SHAPE_UNKNOWN) return FR_STRUCT_ZERO(frRaycastHit); + + frRaycastHit result = { .shape = s, .distance = FLT_MAX }; + + ray.direction = frVec2Normalize(ray.direction); + + float distance = FLT_MAX; + + if (frGetShapeType(s) == FR_SHAPE_CIRCLE) { + bool intersects = frComputeIntersectionRayCircle( + ray.origin, ray.direction, + tx.position, frGetCircleRadius(s), + &distance + ); + + result.check = (distance >= 0.0f) && (distance <= ray.maxDistance); + result.inside = (distance < 0.0f); + + if (result.check) { + result.distance = distance; + + result.point = frVec2Add( + ray.origin, + frVec2ScalarMultiply(ray.direction, result.distance) + ); + + result.normal = frVec2LeftNormal(frVec2Subtract(ray.origin, result.point)); + } + + return result; + } else if (frGetShapeType(s) == FR_SHAPE_POLYGON) { + int intersectionCount = 0; + + frVertices vertices = frGetPolygonVertices(s); + + // 다각형의 변 중에 광선과 교차하는 변이 존재하는지 확인한다. + for (int j = vertices.count - 1, i = 0; i < vertices.count; j = i, i++) { + Vector2 v1 = frVec2Transform(vertices.data[i], tx); + Vector2 v2 = frVec2Transform(vertices.data[j], tx); + + Vector2 diff = frVec2Subtract(v1, v2); + + bool intersects = frComputeIntersectionRays( + ray.origin, ray.direction, + v2, diff, + &distance + ); + + if (intersects && distance <= ray.maxDistance) { + if (result.distance > distance) { + result.distance = distance; + + result.point = frVec2Add( + ray.origin, + frVec2ScalarMultiply(ray.direction, result.distance) + ); + + result.normal = frVec2LeftNormal(diff); + } + + intersectionCount++; + } + } + + result.inside = (intersectionCount & 1); + result.check = (!result.inside) && (intersectionCount > 0); + + return result; + } +} + +/* 강체 `b`에 광선을 투사한다. */ +frRaycastHit frComputeBodyRaycast(frBody *b, frRay ray) { + if (b == NULL || frGetBodyShape(b) == NULL) return FR_STRUCT_ZERO(frRaycastHit); + + frRaycastHit result = frComputeShapeRaycast( + frGetBodyShape(b), frGetBodyTransform(b), + ray + ); + + result.body = b; + + return result; +} + +/* 도형 `s1`의 AABB가 `s2`의 AABB와 충돌하는지 확인한다. */ +static bool frCheckCollisionAABB(frShape *s1, frTransform tx1, frShape *s2, frTransform tx2) { + return CheckCollisionRecs(frGetShapeAABB(s1, tx1), frGetShapeAABB(s2, tx2)); +} + +/* 다각형 `s`에서 벡터 `v`와의 내적이 가장 큰 꼭짓점의 인덱스를 반환한다. */ +static int frGetPolygonFurthestIndex(frShape *s, frTransform tx, Vector2 v) { + if (frGetShapeType(s) != FR_SHAPE_POLYGON) return -1; + + frVertices vertices = frGetPolygonVertices(s); + + float maxDot = -FLT_MAX; + int result = 0; + + v = frVec2Rotate(v, -tx.rotation); + + for (int i = 0; i < vertices.count; i++) { + float dot = frVec2DotProduct(vertices.data[i], v); + + if (maxDot < dot) { + maxDot = dot; + result = i; + } + } + + return result; +} + +/* 도형 `s`에서 벡터 `v`와 가장 수직에 가까운 변을 반환한다. */ +static frVertices frGetShapeSignificantEdge(frShape *s, frTransform tx, Vector2 v) { + if (s == NULL || frGetShapeType(s) == FR_SHAPE_UNKNOWN) return FR_STRUCT_ZERO(frVertices); + + frVertices result = FR_STRUCT_ZERO(frVertices); + + if (frGetShapeType(s) == FR_SHAPE_CIRCLE) { + result.data[0] = frVec2Transform(frVec2ScalarMultiply(v, frGetCircleRadius(s)), tx); + result.count = 1; + + return result; + } else if (frGetShapeType(s) == FR_SHAPE_POLYGON) { + int furthestIndex = frGetPolygonFurthestIndex(s, tx, v); + + frVertices vertices = frGetPolygonVertices(s); + + int prevIndex = (furthestIndex == 0) ? vertices.count - 1 : furthestIndex - 1; + int nextIndex = (furthestIndex == vertices.count - 1) ? 0 : furthestIndex + 1; + + Vector2 furthestVertex = frVec2Transform(vertices.data[furthestIndex], tx); + + Vector2 prevVertex = frVec2Transform(vertices.data[prevIndex], tx); + Vector2 nextVertex = frVec2Transform(vertices.data[nextIndex], tx); + + Vector2 leftV = frVec2Normalize(frVec2Subtract(furthestVertex, prevVertex)); + Vector2 rightV = frVec2Normalize(frVec2Subtract(furthestVertex, nextVertex)); + + if (frVec2DotProduct(leftV, v) < frVec2DotProduct(rightV, v)) { + result.data[0] = prevVertex; + result.data[1] = furthestVertex; + } else { + result.data[0] = furthestVertex; + result.data[1] = nextVertex; + } + + result.count = 2; + + return result; + } +} + +/* 다각형 `s1`에서 `s2`로의 충돌 방향과 깊이를 계산한다. */ +static int frGetSeparatingAxisIndex( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2, + float *depth +) { + if (frGetShapeType(s1) != FR_SHAPE_POLYGON || frGetShapeType(s2) != FR_SHAPE_POLYGON) + return -1; + + int result = -1; + + frVertices normals = frGetPolygonNormals(s1); + + float maxDistance = -FLT_MAX; + + for (int i = 0; i < normals.count; i++) { + Vector2 vertex = frVec2Transform(frGetPolygonVertex(s1, i), tx1); + Vector2 normal = frVec2RotateTx(normals.data[i], tx1); + + int furthestIndex = frGetPolygonFurthestIndex(s2, tx2, frVec2Negate(normal)); + Vector2 furthestVertex = frVec2Transform(frGetPolygonVertex(s2, furthestIndex), tx2); + + float distance = frVec2DotProduct(normal, frVec2Subtract(furthestVertex, vertex)); + + if (maxDistance < distance) { + maxDistance = distance; + result = i; + } + } + + if (depth != NULL) *depth = maxDistance; + + return result; +} + +/* 최적화된 분리 축 정리를 이용하여, 원 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionCirclesSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +) { + if (frGetShapeType(s1) != FR_SHAPE_CIRCLE || frGetShapeType(s2) != FR_SHAPE_CIRCLE) + return FR_STRUCT_ZERO(frCollision); + + frCollision result = FR_STRUCT_ZERO(frCollision); + + Vector2 diff = frVec2Subtract(tx2.position, tx1.position); + + float radiusSum = frGetCircleRadius(s1) + frGetCircleRadius(s2); + float magnitudeSqr = frVec2MagnitudeSqr(diff); + + if (radiusSum * radiusSum < magnitudeSqr) return result; + + float diffMagnitude = sqrtf(magnitudeSqr); + + result.check = true; + + if (diffMagnitude > 0.0f) { + result.direction = frVec2ScalarMultiply(diff, 1.0f / diffMagnitude); + + frVertices e = frGetShapeSignificantEdge(s1, tx1, result.direction); + + result.points[0] = result.points[1] = e.data[0]; + result.depths[0] = result.depths[1] = radiusSum - diffMagnitude; + } else { + result.direction = (Vector2) { .x = 1.0f }; + + frVertices e = frGetShapeSignificantEdge(s1, tx1, result.direction); + + result.points[0] = result.points[1] = e.data[0]; + result.depths[0] = result.depths[1] = frGetCircleRadius(s1); + } + + result.count = 1; + + return result; +} + +/* 최적화된 분리 축 정리를 이용하여, 원 `s1`에서 다각형 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionCirclePolySAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +) { + if (frGetShapeType(s1) == FR_SHAPE_UNKNOWN || frGetShapeType(s2) == FR_SHAPE_UNKNOWN + || frGetShapeType(s1) == frGetShapeType(s2)) + return FR_STRUCT_ZERO(frCollision); + + frCollision result = FR_STRUCT_ZERO(frCollision); + + frShape *circle = s1, *polygon = s2; + frTransform circleTx = tx1, polygonTx = tx2; + + if (frGetShapeType(s1) == FR_SHAPE_POLYGON && frGetShapeType(s2) == FR_SHAPE_CIRCLE) { + circle = s2, polygon = s1; + circleTx = tx2, polygonTx = tx1; + } + + frVertices vertices = frGetPolygonVertices(polygon); + frVertices normals = frGetPolygonNormals(polygon); + + // 다각형의 꼭짓점과 법선 벡터를 변환하는 대신, 원의 중심을 다각형을 기준으로 변환한다. + Vector2 center = frVec2Rotate( + frVec2Subtract(circleTx.position, polygonTx.position), + -polygonTx.rotation + ); + + float radius = frGetCircleRadius(circle), maxDistance = -FLT_MAX; + int normalIndex = -1; + + // 다각형에서 원의 중심과 거리가 가장 가까운 변을 찾는다. + for (int i = 0; i < vertices.count; i++) { + float distance = frVec2DotProduct( + normals.data[i], + frVec2Subtract(center, vertices.data[i]) + ); + + // 꼭짓점 하나라도 원의 바깥쪽에 있다면, 계산을 종료한다. + if (distance > radius) return result; + + if (maxDistance < distance) { + maxDistance = distance; + normalIndex = i; + } + } + + // 다각형이 원의 중심을 포함하고 있다면, 계산을 종료한다. + if (maxDistance < 0.0f) { + result.check = true; + + result.direction = frVec2Negate(frVec2RotateTx(normals.data[normalIndex], polygonTx)); + + if (frVec2DotProduct(frVec2Subtract(tx2.position, tx1.position), result.direction) < 0.0f) + result.direction = frVec2Negate(result.direction); + + result.points[0] = result.points[1] = frVec2Add( + circleTx.position, + frVec2ScalarMultiply(result.direction, -radius) + ); + + result.depths[0] = result.depths[1] = radius - maxDistance; + + result.count = 1; + } else { + Vector2 v1 = (normalIndex > 0) + ? vertices.data[normalIndex - 1] + : vertices.data[vertices.count - 1]; + Vector2 v2 = vertices.data[normalIndex]; + + /* + 1. 원이 `v1`을 포함하는 선분과 만나는가? + 2. 원이 `v2`를 포함하는 선분과 만나는가? + 3. 원이 `v1`과 `v2` 사이의 선분과 만나는가? + + => 벡터의 내적을 통해 확인할 수 있다. + */ + Vector2 diff1 = frVec2Subtract(center, v1), diff2 = frVec2Subtract(center, v2); + + float v1Dot = frVec2DotProduct(diff1, frVec2Subtract(v2, v1)); + float v2Dot = frVec2DotProduct(diff2, frVec2Subtract(v1, v2)); + + if (v1Dot <= 0.0f) { + float magnitudeSqr = frVec2MagnitudeSqr(diff1); + + if (magnitudeSqr > radius * radius) return result; + + result.direction = frVec2Normalize(frVec2RotateTx(frVec2Negate(diff1), polygonTx)); + + if (frVec2DotProduct(frVec2Subtract(tx2.position, tx1.position), result.direction) < 0.0f) + result.direction = frVec2Negate(result.direction); + + result.points[0] = result.points[1] = frVec2Transform(v1, polygonTx); + result.depths[0] = result.depths[1] = radius - sqrtf(magnitudeSqr); + } else if (v2Dot <= 0.0f) { + float magnitudeSqr = frVec2MagnitudeSqr(diff2); + + if (magnitudeSqr > radius * radius) return result; + + result.direction = frVec2Normalize(frVec2RotateTx(frVec2Negate(diff2), polygonTx)); + + if (frVec2DotProduct(frVec2Subtract(tx2.position, tx1.position), result.direction) < 0.0f) + result.direction = frVec2Negate(result.direction); + + result.points[0] = result.points[1] = frVec2Transform(v2, polygonTx); + result.depths[0] = result.depths[1] = radius - sqrtf(magnitudeSqr); + } else { + Vector2 normal = normals.data[normalIndex]; + + if (frVec2DotProduct(diff1, normal) > radius) return result; + + result.direction = frVec2Negate(frVec2RotateTx(normal, polygonTx)); + + if (frVec2DotProduct(frVec2Subtract(tx2.position, tx1.position), result.direction) < 0.0f) + result.direction = frVec2Negate(result.direction); + + result.points[0] = result.points[1] = frVec2Add( + circleTx.position, + frVec2ScalarMultiply(result.direction, -radius) + ); + + result.depths[0] = result.depths[1] = radius - maxDistance; + } + + result.check = true; + + result.count = 1; + } + + return result; +} + +/* 최적화된 분리 축 정리를 이용하여, 다각형 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionPolysSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +) { + if (frGetShapeType(s1) != FR_SHAPE_POLYGON || frGetShapeType(s2) != FR_SHAPE_POLYGON) + return FR_STRUCT_ZERO(frCollision); + + frCollision result = FR_STRUCT_ZERO(frCollision); + + float depth1 = FLT_MAX, depth2 = FLT_MAX; + + int index1 = frGetSeparatingAxisIndex(s1, tx1, s2, tx2, &depth1); + if (depth1 >= 0.0f) return result; + + int index2 = frGetSeparatingAxisIndex(s2, tx2, s1, tx1, &depth2); + if (depth2 >= 0.0f) return result; + + Vector2 direction = (depth1 > depth2) + ? frVec2RotateTx(frGetPolygonNormal(s1, index1), tx1) + : frVec2RotateTx(frGetPolygonNormal(s2, index2), tx2); + + if (frVec2DotProduct(frVec2Subtract(tx2.position, tx1.position), direction) < 0.0f) + direction = frVec2Negate(direction); + + result.check = true; + + // 회전 변환이 적용된 벡터는 회전 변환을 적용하기 전의 벡터와 크기가 같다. + result.direction = direction; + + frVertices e1 = frGetShapeSignificantEdge(s1, tx1, direction); + frVertices e2 = frGetShapeSignificantEdge(s2, tx2, frVec2Negate(direction)); + + frVertices refEdge = e1, incEdge = e2; + frTransform refTx = tx1, incTx = tx2; + + float dot1 = frVec2DotProduct(frVec2Subtract(e1.data[1], e1.data[0]), direction); + float dot2 = frVec2DotProduct(frVec2Subtract(e2.data[1], e2.data[0]), direction); + + if (fabsf(dot1) > fabsf(dot2)) { + refEdge = e2; + incEdge = e1; + + refTx = tx2; + incTx = tx1; + } + + Vector2 refV = frVec2Normalize(frVec2Subtract(refEdge.data[1], refEdge.data[0])); + + float refDot1 = frVec2DotProduct(refEdge.data[0], refV); + float refDot2 = frVec2DotProduct(refEdge.data[1], refV); + + incEdge = frClipEdgeWithAxis(incEdge, refV, refDot1); + incEdge = frClipEdgeWithAxis(incEdge, frVec2Negate(refV), -refDot2); + + Vector2 refNormal = frVec2RightNormal(refV); + + float max_depth = frVec2DotProduct(refEdge.data[0], refNormal); + + result.points[0] = incEdge.data[0], result.points[1] = incEdge.data[1]; + + result.depths[0] = frVec2DotProduct(result.points[0], refNormal) - max_depth; + result.depths[1] = frVec2DotProduct(result.points[1], refNormal) - max_depth; + + result.count = 2; + + if (result.depths[0] < 0.0f) { + result.points[0] = result.points[1]; + result.depths[0] = result.depths[1]; + + result.count = 1; + } else if (result.depths[1] < 0.0f) { + result.points[1] = result.points[0]; + result.depths[1] = result.depths[0]; + + result.count = 1; + } + + return result; +} + +/* 최적화된 분리 축 정리를 이용하여, 도형 `s1`에서 `s2`로의 충돌을 계산한다. */ +static frCollision frComputeCollisionSAT( + frShape *s1, frTransform tx1, + frShape *s2, frTransform tx2 +) { + frShapeType type1 = frGetShapeType(s1); + frShapeType type2 = frGetShapeType(s2); + + if (type1 == FR_SHAPE_UNKNOWN || type2 == FR_SHAPE_UNKNOWN) + return FR_STRUCT_ZERO(frCollision); + else if (type1 == FR_SHAPE_CIRCLE && type2 == FR_SHAPE_CIRCLE) + return frComputeCollisionCirclesSAT(s1, tx1, s2, tx2); + else if (type1 == FR_SHAPE_CIRCLE && type2 == FR_SHAPE_POLYGON + || type1 == FR_SHAPE_POLYGON && type2 == FR_SHAPE_CIRCLE) + return frComputeCollisionCirclePolySAT(s1, tx1, s2, tx2); + else if (type1 == FR_SHAPE_POLYGON && type2 == FR_SHAPE_POLYGON) + return frComputeCollisionPolysSAT(s1, tx1, s2, tx2); +} + +/* 선분 `e`에서 벡터 `v`와의 내적이 `minDot`보다 작은 부분을 모두 잘라낸다. */ +static frVertices frClipEdgeWithAxis(frVertices e, Vector2 v, float minDot) { + frVertices result = FR_STRUCT_ZERO(frVertices); + + float p1Dot = frVec2DotProduct(e.data[0], v) - minDot; + float p2Dot = frVec2DotProduct(e.data[1], v) - minDot; + + bool insideP1 = (p1Dot >= 0.0f), insideP2 = (p2Dot >= 0.0f); + + if (insideP1 && insideP2) { + result.data[0] = e.data[0]; + result.data[1] = e.data[1]; + + result.count = 2; + } else { + Vector2 edgeV = frVec2Subtract(e.data[1], e.data[0]); + + const float distance = p1Dot / (p1Dot - p2Dot); + + Vector2 midpoint = frVec2Add( + e.data[0], + frVec2ScalarMultiply(edgeV, distance) + ); + + if (insideP1 && !insideP2) { + result.data[0] = e.data[0]; + result.data[1] = midpoint; + + result.count = 2; + } else if (!insideP1 && insideP2) { + result.data[0] = e.data[1]; + result.data[1] = midpoint; + + result.count = 2; + } + } + + return result; +} + +/* `o1`에서 `v1` 방향으로 진행하는 광선이 `o2`에서 `v2` 방향으로 진행하는 광선과 만나는지 계산한다. */ +static bool frComputeIntersectionRays( + Vector2 o1, Vector2 v1, + Vector2 o2, Vector2 v2, + float *distance +) { + bool result = true; + + float cross = frVec2CrossProduct(v1, v2); + + // 두 광선은 평행하거나 일직선 상에 있다. + if (frNumberApproxEquals(cross, 0.0f)) { + *distance = 0.0f; + + return false; + } + + Vector2 diff = frVec2Subtract(o2, o1); + + float inverseCross = 1.0f / cross; + + float t = frVec2CrossProduct(diff, v2) * inverseCross; + float u = frVec2CrossProduct(diff, v1) * inverseCross; + + // 두 광선은 평행하지 않으면서 서로 만나지 않는다. + if (t < 0.0f || u < 0.0f || u > 1.0f) result = false; + + *distance = t; + + return result; +} + +/* `o`에서 `v` 방향으로 진행하는 광선이 중심점이 `c`이고 반지름이 `r`인 원과 만나는지 계산한다. */ +static bool frComputeIntersectionRayCircle( + Vector2 o, Vector2 v, + Vector2 c, float r, + float *distance +) { + bool result = true; + + Vector2 diff = frVec2Subtract(c, o); + + float dot = frVec2DotProduct(diff, v); + + float heightSqr = frVec2MagnitudeSqr(diff) - (dot * dot); + float baseSqr = (r * r) - heightSqr; + + // 광선과 원은 서로 만나지 않는다. + if (dot < 0.0f || baseSqr < 0.0f) result = false; + + *distance = dot - sqrtf(baseSqr); + + return result; +} diff --git a/code/vendors/ferox/src/debug.c b/code/vendors/ferox/src/debug.c new file mode 100644 index 0000000..3c0e716 --- /dev/null +++ b/code/vendors/ferox/src/debug.c @@ -0,0 +1,195 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +/* | `debug` 모듈 변수... | */ + +static const float ARROW_HEAD_LENGTH = 16.0f; + +/* | `debug` 모듈 함수... | */ + +#ifndef FEROX_STANDALONE + /* 강체 `b`의 다각형 꼭짓점 배열을 세계 기준의 픽셀 좌표 배열로 변환한다. */ + static frVertices frGetWorldVerticesInPixels(frBody *b); + + /* 게임 화면에 점 `p1`에서 `p2`로 향하는 화살표를 그린다. */ + void frDrawArrow(Vector2 p1, Vector2 p2, float thick, Color color) { + if (thick <= 0.0f) return; + + p1 = frVec2MetersToPixels(p1); + p2 = frVec2MetersToPixels(p2); + + Vector2 diff = frVec2Normalize(frVec2Subtract(p1, p2)); + + Vector2 normal1 = frVec2LeftNormal(diff); + Vector2 normal2 = frVec2RightNormal(diff); + + Vector2 h1 = frVec2Add( + p2, + frVec2ScalarMultiply( + frVec2Normalize(frVec2Add(diff, normal1)), + ARROW_HEAD_LENGTH + ) + ); + + Vector2 h2 = frVec2Add( + p2, + frVec2ScalarMultiply( + frVec2Normalize(frVec2Add(diff, normal2)), + ARROW_HEAD_LENGTH + ) + ); + + DrawLineEx(p1, p2, thick, color); + DrawLineEx(p2, h1, thick, color); + DrawLineEx(p2, h2, thick, color); + } + + /* 게임 화면에 강체 `b`의 도형을 그린다. */ + void frDrawBody(frBody *b, Color color) { + if (b == NULL) return; + + frShape *s = frGetBodyShape(b); + + if (frGetShapeType(s) == FR_SHAPE_CIRCLE) { + DrawCircleV( + frVec2MetersToPixels(frGetBodyPosition(b)), + frNumberMetersToPixels(frGetCircleRadius(s)), + color + ); + } else if (frGetShapeType(s) == FR_SHAPE_POLYGON) { + frVertices worldVertices = frGetWorldVerticesInPixels(b); + + DrawTriangleFan(worldVertices.data, worldVertices.count, color); + } + } + + /* 게임 화면에 강체 `b`의 도형 테두리를 그린다. */ + void frDrawBodyLines(frBody *b, float thick, Color color) { + if (b == NULL || thick <= 0.0f) return; + + frShape *s = frGetBodyShape(b); + + if (frGetShapeType(s) == FR_SHAPE_CIRCLE) { + Vector2 p = frGetBodyPosition(b); + + DrawRing( + frVec2MetersToPixels(p), + frNumberMetersToPixels(frGetCircleRadius(s)) - thick, + frNumberMetersToPixels(frGetCircleRadius(s)), + 0.0f, + 360.0f, + FR_DEBUG_CIRCLE_SEGMENT_COUNT, + color + ); + } else if (frGetShapeType(s) == FR_SHAPE_POLYGON) { + frVertices worldVertices = frGetWorldVerticesInPixels(b); + + for (int j = worldVertices.count - 1, i = 0; i < worldVertices.count; j = i, i++) + DrawLineEx(worldVertices.data[j], worldVertices.data[i], thick, color); + } + } + + /* 게임 화면에 강체 `b`의 AABB와 질량 중심을 그린다. */ + void frDrawBodyAABB(frBody *b, float thick, Color color) { + if (b == NULL || thick <= 0.0f) return; + + Rectangle aabb = frGetBodyAABB(b); + + DrawRectangleLinesEx( + (Rectangle) { + frNumberMetersToPixels(aabb.x), + frNumberMetersToPixels(aabb.y), + frNumberMetersToPixels(aabb.width), + frNumberMetersToPixels(aabb.height) + }, + thick, + color + ); + + DrawCircleV(frVec2MetersToPixels(frGetBodyPosition(b)), 2.0f, color); + } + + /* 게임 화면에 강체 `b`의 물리량 정보를 그린다. */ + void frDrawBodyProperties(frBody *b, Color color) { + Vector2 velocity = frGetBodyVelocity(b); + + frTransform tx = frGetBodyTransform(b); + + DrawTextEx( + GetFontDefault(), + TextFormat( + "m: %fkg, I: %fkg*m²\n" + "(x, y) [theta]: (%f, %f) [%f rad.]\n" + "v [omega]: (%f, %f) [%f rad.]\n", + frGetBodyMass(b), frGetBodyInertia(b), + tx.position.x, tx.position.y, tx.rotation, + velocity.x, velocity.y, frGetBodyAngularVelocity(b) + ), + frVec2MetersToPixels(frVec2Add(tx.position, (Vector2) { 1.0f, 1.0f })), + 10.0f, + 1.0f, + color + ); + } + + /* 게임 화면에 공간 해시맵 `hm`을 그린다. */ + void frDrawSpatialHash(frSpatialHash *hm, float thick, Color color) { + if (hm == NULL || thick <= 0.0f) return; + + Rectangle bounds = frGetSpatialHashBounds(hm); + + const int vCount = bounds.width * FR_BROADPHASE_INVERSE_CELL_SIZE; + const int hCount = bounds.height * FR_BROADPHASE_INVERSE_CELL_SIZE; + + for (int i = 0; i <= vCount; i++) + DrawLineEx( + frVec2MetersToPixels((Vector2) { FR_BROADPHASE_CELL_SIZE * i, 0.0f }), + frVec2MetersToPixels((Vector2) { FR_BROADPHASE_CELL_SIZE * i, bounds.height }), + thick, + color + ); + + for (int i = 0; i <= hCount; i++) + DrawLineEx( + frVec2MetersToPixels((Vector2) { 0.0f, FR_BROADPHASE_CELL_SIZE * i }), + frVec2MetersToPixels((Vector2) { bounds.width, FR_BROADPHASE_CELL_SIZE * i }), + thick, + color + ); + + DrawRectangleLinesEx(frRecMetersToPixels(bounds), thick, color); + } + + /* 강체 `b`의 다각형 꼭짓점 배열을 세계 기준의 픽셀 좌표 배열로 변환한다. */ + static frVertices frGetWorldVerticesInPixels(frBody *b) { + if (b == NULL) return FR_STRUCT_ZERO(frVertices); + + frVertices vertices = frGetPolygonVertices(frGetBodyShape(b)); + + for (int i = 0; i < vertices.count; i++) + vertices.data[i] = frVec2MetersToPixels(frGetWorldPoint(b, vertices.data[i])); + + return vertices; + } +#endif diff --git a/code/vendors/ferox/src/dynamics.c b/code/vendors/ferox/src/dynamics.c new file mode 100644 index 0000000..c3c6b1b --- /dev/null +++ b/code/vendors/ferox/src/dynamics.c @@ -0,0 +1,529 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +/* | `dynamics` 모듈 자료형 정의... | */ + +/* 강체의 물리량을 나타내는 구조체. */ +typedef struct frMotionData { + float mass; + float inverseMass; + float inertia; + float inverseInertia; + Vector2 velocity; + float angularVelocity; + float gravityScale; + Vector2 force; + float torque; +} frMotionData; + +/* 강체를 나타내는 구조체. */ +typedef struct frBody { + frBodyType type; + frBodyFlags flags; + frMaterial material; + frMotionData motion; + frTransform tx; + frShape *shape; + Rectangle aabb; + void *data; +} frBody; + +/* | `dynamics` 모듈 함수... | */ + +/* 강체 `b`의 질량을 다시 계산한다. */ +static void frResetBodyMass(frBody *b); + +/* 종류가 `type`이고 위치가 `p`인 강체를 생성한다. */ +frBody *frCreateBody(frBodyType type, frBodyFlags flags, Vector2 p) { + if (type == FR_BODY_UNKNOWN) return NULL; + + frBody *result = calloc(1, sizeof(*result)); + + result->material = FR_STRUCT_ZERO(frMaterial); + + frSetBodyType(result, type); + frSetBodyFlags(result, flags); + frSetBodyGravityScale(result, 1.0f); + frSetBodyPosition(result, p); + + return result; +} + +/* 종류가 `type`이고 위치가 `p`이며 충돌 처리용 도형이 `s`인 강체를 생성한다. */ +frBody *frCreateBodyFromShape(frBodyType type, frBodyFlags flags, Vector2 p, frShape *s) { + if (type == FR_BODY_UNKNOWN) return NULL; + + frBody *result = frCreateBody(type, flags, p); + frAttachShapeToBody(result, s); + + return result; +} + +/* 강체 `b`에 할당된 메모리를 해제한다. */ +void frReleaseBody(frBody *b) { + if (b != NULL && b->shape != NULL) + frDetachShapeFromBody(b); + + free(b); +} + +/* 강체 `b에 충돌 처리용 도형 `s`를 추가한다. */ +void frAttachShapeToBody(frBody *b, frShape *s) { + if (b == NULL || s == NULL) return; + + b->shape = s; + b->material = frGetShapeMaterial(s); + b->aabb = frGetShapeAABB(b->shape, b->tx); + + frResetBodyMass(b); +} + +/* 강체 `b`에서 충돌 처리용 도형을 제거한다. */ +void frDetachShapeFromBody(frBody *b) { + if (b == NULL) return; + + b->shape = NULL; + b->material = FR_STRUCT_ZERO(frMaterial); + b->aabb = FR_STRUCT_ZERO(Rectangle); + + frResetBodyMass(b); +} + +/* 구조체 `frBody`의 크기를 반환한다. */ +size_t frGetBodyStructSize(void) { + return sizeof(frBody); +} + +/* 강체 `b`의 종류를 반환한다. */ +frBodyType frGetBodyType(frBody *b) { + return (b != NULL) ? b->type : FR_BODY_UNKNOWN; +} + +/* 강체 `b`의 비트 플래그 조합을 반환한다. */ +frBodyFlags frGetBodyFlags(frBody *b) { + return (b != NULL) ? b->flags : 0; +} + +/* 강체 `b`의 재질을 반환한다. */ +frMaterial frGetBodyMaterial(frBody *b) { + return (b != NULL) ? b->material : FR_STRUCT_ZERO(frMaterial); +} + +/* 강체 `b`의 질량을 반환한다. */ +float frGetBodyMass(frBody *b) { + return (b != NULL) ? b->motion.mass : 0.0f; +} + +/* 강체 `b`의 질량의 역수를 반환한다. */ +float frGetBodyInverseMass(frBody *b) { + return (b != NULL) ? b->motion.inverseMass : 0.0f; +} + +/* 강체 `b`의 관성 모멘트를 반환한다. */ +float frGetBodyInertia(frBody *b) { + return (b != NULL) ? b->motion.inertia : 0.0f; +} + +/* 강체 `b`의 관성 모멘트의 역수를 반환한다. */ +float frGetBodyInverseInertia(frBody *b) { + return (b != NULL) ? b->motion.inverseInertia : 0.0f; +} + +/* 강체 `b`의 속도를 반환한다. */ +Vector2 frGetBodyVelocity(frBody *b) { + return (b != NULL) ? b->motion.velocity : FR_STRUCT_ZERO(Vector2); +} + +/* 강체 `b`의 각속도를 반환한다. */ +float frGetBodyAngularVelocity(frBody *b) { + return (b != NULL) ? b->motion.angularVelocity : 0.0f; +} + +/* 강체 `b`의 중력 가속률을 반환한다. */ +float frGetBodyGravityScale(frBody *b) { + return (b != NULL) ? b->motion.gravityScale : 0.0f; +} + +/* 강체 `b`의 위치와 회전 각도 (단위: rad.)를 반환한다. */ +frTransform frGetBodyTransform(frBody *b) { + return (b != NULL) ? b->tx : FR_STRUCT_ZERO(frTransform); +} + +/* 강체 `b`의 위치를 반환한다. */ +Vector2 frGetBodyPosition(frBody *b) { + return (b != NULL) ? b->tx.position : FR_STRUCT_ZERO(Vector2); +} + +/* 강체 `b`의 회전 각도 (단위: rad.)를 반환한다. */ +float frGetBodyRotation(frBody *b) { + return (b != NULL) ? b->tx.rotation : 0.0f; +} + +/* 강체 `b`의 충돌 처리용 도형을 반환한다. */ +frShape *frGetBodyShape(frBody *b) { + return (b != NULL) ? b->shape : NULL; +} + +/* 강체 `b`의 AABB를 반환한다. */ +Rectangle frGetBodyAABB(frBody *b) { + return (b != NULL && b->shape != NULL) ? b->aabb : FR_STRUCT_ZERO(Rectangle); +} + +/* 강체 `b`의 사용자 데이터를 반환한다. */ +void *frGetBodyUserData(frBody *b) { + return (b != NULL) ? b->data : NULL; +} + +/* 세계 기준 좌표 `p`를 강체 `b`를 기준으로 한 좌표로 변환한다. */ +Vector2 frGetLocalPoint(frBody *b, Vector2 p) { + return frVec2Transform(p, (frTransform) { frVec2Negate(b->tx.position), -(b->tx.rotation)}); +} + +/* 강체 `b`를 기준으로 한 좌표 `p`를 세계 기준 좌표로 변환한다. */ +Vector2 frGetWorldPoint(frBody *b, Vector2 p) { + return frVec2Transform(p, b->tx); +} + +/* 강체 `b`의 종류를 `type`으로 설정한다. */ +void frSetBodyType(frBody *b, frBodyType type) { + if (b == NULL || b->type == FR_BODY_UNKNOWN || b->type == type) + return; + + b->type = type; + + frResetBodyMass(b); +} + +/* 강체 `b`의 비트 플래그 조합을 `flags`으로 설정한다. */ +void frSetBodyFlags(frBody *b, frBodyFlags flags) { + if (b == NULL || b->flags == flags) return; + + b->flags = flags; + + frResetBodyMass(b); +} + +/* 강체 `b`의 속도를 `v`로 설정한다. */ +void frSetBodyVelocity(frBody *b, Vector2 v) { + if (b != NULL) b->motion.velocity = v; +} + +/* 강체 `b`의 각속도를 `a`로 설정한다. */ +void frSetBodyAngularVelocity(frBody *b, double a) { + if (b != NULL) b->motion.angularVelocity = a; +} + +/* 강체 `b`의 중력 가속률을 `scale`로 설정한다. */ +void frSetBodyGravityScale(frBody *b, float scale) { + if (b == NULL || b->type != FR_BODY_DYNAMIC) return; + + b->motion.gravityScale = scale; +} + +/* 강체 `b`의 위치와 회전 각도를 `tx`의 값으로 설정한다. */ +void frSetBodyTransform(frBody *b, frTransform tx) { + if (b == NULL) return; + + b->tx.position = tx.position; + b->tx.rotation = frNormalizeAngle(tx.rotation, PI); + + b->tx.cache.valid = true; + b->tx.cache.sinA = sinf(b->tx.rotation); + b->tx.cache.cosA = cosf(b->tx.rotation); + + b->aabb = frGetShapeAABB(b->shape, b->tx); +} + +/* 강체 `b`의 위치를 `p`로 설정한다. */ +void frSetBodyPosition(frBody *b, Vector2 p) { + if (b == NULL) return; + + b->tx.position = p; + + b->aabb = frGetShapeAABB(b->shape, b->tx); +} + +/* 강체 `b`의 회전 각도 (단위: rad.)를 `rotation`으로 설정한다. */ +void frSetBodyRotation(frBody *b, float rotation) { + if (b == NULL) return; + + b->tx.rotation = frNormalizeAngle(rotation, PI); + + b->tx.cache.valid = true; + b->tx.cache.sinA = sinf(b->tx.rotation); + b->tx.cache.cosA = cosf(b->tx.rotation); + + b->aabb = frGetShapeAABB(b->shape, b->tx); +} + +/* 강체 `b`의 사용자 데이터를 `data`로 설정한다. */ +void frSetBodyUserData(frBody *b, void *data) { + if (b != NULL) b->data = data; +} + +/* 강체 `b`에 중력 가속도 `gravity`를 적용한다. */ +void frApplyGravity(frBody *b, Vector2 gravity) { + if (b == NULL || b->motion.inverseMass <= 0.0f) return; + + b->motion.force = frVec2Add( + b->motion.force, + frVec2ScalarMultiply( + gravity, + b->motion.gravityScale * b->motion.mass + ) + ); +} + +/* 강체 `b`에 충격량 `impulse`를 적용한다. */ +void frApplyImpulse(frBody *b, Vector2 impulse) { + if (b == NULL || b->motion.inverseMass <= 0.0f) return; + + b->motion.velocity = frVec2Add( + b->motion.velocity, + frVec2ScalarMultiply( + impulse, + b->motion.inverseMass + ) + ); +} + +/* 강체 `b` 위의 점 `p`에 각운동량 `impulse`를 적용한다. */ +void frApplyTorqueImpulse(frBody *b, Vector2 p, Vector2 impulse) { + if (b == NULL || b->motion.inverseInertia <= 0.0f) return; + + b->motion.angularVelocity += b->motion.inverseInertia + * frVec2CrossProduct(p, impulse); +} + +/* 강체 `b`에 작용하는 모든 힘을 제거한다. */ +void frClearBodyForces(frBody *b) { + if (b == NULL) return; + + b->motion.force = FR_STRUCT_ZERO(Vector2); + b->motion.torque = 0.0f; +} + +/* 단위 시간 `dt` 이후의 강체 `b`의 위치를 계산한다. */ +void frIntegrateForBodyPosition(frBody *b, double dt) { + if (b == NULL || b->type == FR_BODY_STATIC) return; + + frSetBodyPosition(b, frVec2Add(b->tx.position, frVec2ScalarMultiply(b->motion.velocity, dt))); + frSetBodyRotation(b, b->tx.rotation + (b->motion.angularVelocity * dt)); +} + +/* 단위 시간 `dt` 이후의 강체 `b`의 속도와 각속도를 계산한다. */ +void frIntegrateForBodyVelocities(frBody *b, double dt) { + if (b == NULL || b->motion.inverseMass <= 0.0f) return; + + b->motion.velocity = frVec2Add( + b->motion.velocity, + frVec2ScalarMultiply( + b->motion.force, + b->motion.inverseMass * dt + ) + ); + + b->motion.angularVelocity += (b->motion.torque * b->motion.inverseInertia) * dt; +} + +/* 강체 `b1`과 `b2` 사이의 충돌을 해결한다. */ +void frResolveCollision(frCollision *collision) { + if (collision == NULL || !collision->check) return; + + frBody *b1 = collision->cache.bodies[0]; + frBody *b2 = collision->cache.bodies[1]; + + if (b1 == NULL || b2 == NULL) return; + + if (b1->motion.inverseMass + b2->motion.inverseMass <= 0.0f) { + if (frGetBodyType(b1) == FR_BODY_STATIC) b1->motion.velocity = FR_STRUCT_ZERO(Vector2); + if (frGetBodyType(b2) == FR_BODY_STATIC) b2->motion.velocity = FR_STRUCT_ZERO(Vector2); + + return; + } + + float epsilon = fmaxf(0.0f, fminf(b1->material.restitution, b2->material.restitution)); + + float staticMu = b1->material.staticFriction * b2->material.staticFriction; + float dynamicMu = b1->material.dynamicFriction * b2->material.dynamicFriction; + + // 대체로 운동 마찰 계수는 정지 마찰 계수보다 작은 값을 가진다. + if (staticMu < dynamicMu) dynamicMu = FR_DYNAMICS_DYNAMIC_FRICTION_MULTIPLIER * staticMu; + + for (int i = 0; i < collision->count; i++) { + Vector2 r1 = frVec2Subtract(collision->points[i], frGetBodyPosition(b1)); + Vector2 r2 = frVec2Subtract(collision->points[i], frGetBodyPosition(b2)); + + Vector2 r1Normal = frVec2LeftNormal(r1); + Vector2 r2Normal = frVec2LeftNormal(r2); + + // `b1`이 측정한 `b2`의 속도 (`b1`에 대한 `b2`의 상대 속도)를 계산한다. + Vector2 relativeVelocity = frVec2Subtract( + frVec2Add( + b2->motion.velocity, + frVec2ScalarMultiply(r2Normal, b2->motion.angularVelocity) + ), + frVec2Add( + b1->motion.velocity, + frVec2ScalarMultiply(r1Normal, b1->motion.angularVelocity) + ) + ); + + float relativeVelocityDot = frVec2DotProduct(relativeVelocity, collision->direction); + + // 두 강체가 서로 충돌하는 방향으로 진행하고 있지 않으면 계산을 종료한다. + if (relativeVelocityDot > 0.0f) return; + + float r1NormalDot = frVec2DotProduct(r1Normal, collision->direction); + float r2NormalDot = frVec2DotProduct(r2Normal, collision->direction); + + float inverseMassSum = (b1->motion.inverseMass + b2->motion.inverseMass) + + b1->motion.inverseInertia * (r1NormalDot * r1NormalDot) + + b2->motion.inverseInertia * (r2NormalDot * r2NormalDot); + + float impulseParam = (-(1.0f + epsilon) * relativeVelocityDot) + / (collision->count * inverseMassSum); + + Vector2 impulse = frVec2ScalarMultiply(collision->direction, impulseParam); + + frApplyImpulse(b1, frVec2Negate(impulse)); + frApplyTorqueImpulse(b1, r1, frVec2Negate(impulse)); + + frApplyImpulse(b2, impulse); + frApplyTorqueImpulse(b2, r2, impulse); + + // 마찰력 적용을 위해 상대 속도를 다시 계산한다. + relativeVelocity = frVec2Subtract( + frVec2Add( + b2->motion.velocity, + frVec2ScalarMultiply(r2Normal, b2->motion.angularVelocity) + ), + frVec2Add( + b1->motion.velocity, + frVec2ScalarMultiply(r1Normal, b1->motion.angularVelocity) + ) + ); + + relativeVelocityDot = frVec2DotProduct(relativeVelocity, collision->direction); + + Vector2 tangent = frVec2Normalize( + frVec2Subtract( + relativeVelocity, + frVec2ScalarMultiply(collision->direction, relativeVelocityDot) + ) + ); + + r1NormalDot = frVec2DotProduct(r1Normal, tangent); + r2NormalDot = frVec2DotProduct(r2Normal, tangent); + + inverseMassSum = (b1->motion.inverseMass + b2->motion.inverseMass) + + b1->motion.inverseInertia * (r1NormalDot * r1NormalDot) + + b2->motion.inverseInertia * (r2NormalDot * r2NormalDot); + + float frictionParam = -frVec2DotProduct(relativeVelocity, tangent) + / (collision->count * inverseMassSum); + + Vector2 friction = (fabsf(frictionParam) < impulseParam * staticMu) + ? frVec2ScalarMultiply(tangent, frictionParam) + : frVec2ScalarMultiply(tangent, -impulseParam * dynamicMu); + + frApplyImpulse(b1, frVec2Negate(friction)); + frApplyTorqueImpulse(b1, r1, frVec2Negate(friction)); + + frApplyImpulse(b2, friction); + frApplyTorqueImpulse(b2, r2, friction); + } +} + +/* 강체 `b1`과 `b2`의 위치를 적절하게 보정한다. */ +void frCorrectBodyPositions(frCollision *collision, float inverseDt) { + if (collision == NULL || !collision->check) return; + + frBody *b1 = collision->cache.bodies[0]; + frBody *b2 = collision->cache.bodies[1]; + + if (b1 == NULL || b2 == NULL) return; + + if (b1->motion.inverseMass + b2->motion.inverseMass <= 0.0f) { + if (frGetBodyType(b1) == FR_BODY_STATIC) b1->motion.velocity = FR_STRUCT_ZERO(Vector2); + if (frGetBodyType(b2) == FR_BODY_STATIC) b2->motion.velocity = FR_STRUCT_ZERO(Vector2); + + return; + } + + float maxDepth = fmaxf(collision->depths[0], collision->depths[1]); + + if (maxDepth <= FR_DYNAMICS_CORRECTION_DEPTH_THRESHOLD) return; + + // 충돌 방향은 무조건 `b1`에서 `b2`로 향한다. + Vector2 correction = frVec2ScalarMultiply( + collision->direction, + FR_DYNAMICS_CORRECTION_DEPTH_SCALE * ( + (inverseDt * (maxDepth - FR_DYNAMICS_CORRECTION_DEPTH_THRESHOLD)) + / (b1->motion.inverseMass + b2->motion.inverseMass) + ) + ); + + frSetBodyPosition( + b1, + frVec2Subtract( + b1->tx.position, + frVec2ScalarMultiply(correction, b1->motion.inverseMass) + ) + ); + frSetBodyPosition( + b2, + frVec2Add( + b2->tx.position, + frVec2ScalarMultiply(correction, b2->motion.inverseMass) + ) + ); +} + +/* 강체 `b`의 질량을 다시 계산한다. */ +static void frResetBodyMass(frBody *b) { + if (b == NULL) return; + + b->motion.mass = 0.0f; + b->motion.inertia = 0.0f; + + if (b->type == FR_BODY_STATIC) { + b->motion.velocity = FR_STRUCT_ZERO(Vector2); + b->motion.angularVelocity = 0.0f; + } else if (b->type == FR_BODY_DYNAMIC) { + if (b->shape != NULL) { + if (!(b->flags & FR_FLAG_INFINITE_MASS)) + b->motion.mass = frGetShapeMass(b->shape); + + if (!(b->flags & FR_FLAG_INFINITE_INERTIA)) + b->motion.inertia = frGetShapeInertia(b->shape); + } + } + + if (b->motion.mass == 0.0f) b->motion.inverseMass = 0.0f; + else b->motion.inverseMass = 1.0f / b->motion.mass; + + if (b->motion.inertia == 0.0f) b->motion.inverseInertia = 0.0f; + else b->motion.inverseInertia = 1.0f / b->motion.inertia; +} diff --git a/code/vendors/ferox/src/external/sokol_time.h b/code/vendors/ferox/src/external/sokol_time.h new file mode 100644 index 0000000..2d4d456 --- /dev/null +++ b/code/vendors/ferox/src/external/sokol_time.h @@ -0,0 +1,319 @@ +#if defined(SOKOL_IMPL) && !defined(SOKOL_TIME_IMPL) +#define SOKOL_TIME_IMPL +#endif +#ifndef SOKOL_TIME_INCLUDED +/* + sokol_time.h -- simple cross-platform time measurement + + Project URL: https://github.com/floooh/sokol + + Do this: + #define SOKOL_IMPL or + #define SOKOL_TIME_IMPL + before you include this file in *one* C or C++ file to create the + implementation. + + Optionally provide the following defines with your own implementations: + SOKOL_ASSERT(c) - your own assert macro (default: assert(c)) + SOKOL_TIME_API_DECL - public function declaration prefix (default: extern) + SOKOL_API_DECL - same as SOKOL_TIME_API_DECL + SOKOL_API_IMPL - public function implementation prefix (default: -) + + If sokol_time.h is compiled as a DLL, define the following before + including the declaration or implementation: + + SOKOL_DLL + + On Windows, SOKOL_DLL will define SOKOL_TIME_API_DECL as __declspec(dllexport) + or __declspec(dllimport) as needed. + + void stm_setup(); + Call once before any other functions to initialize sokol_time + (this calls for instance QueryPerformanceFrequency on Windows) + + uint64_t stm_now(); + Get current point in time in unspecified 'ticks'. The value that + is returned has no relation to the 'wall-clock' time and is + not in a specific time unit, it is only useful to compute + time differences. + + uint64_t stm_diff(uint64_t new, uint64_t old); + Computes the time difference between new and old. This will always + return a positive, non-zero value. + + uint64_t stm_since(uint64_t start); + Takes the current time, and returns the elapsed time since start + (this is a shortcut for "stm_diff(stm_now(), start)") + + uint64_t stm_laptime(uint64_t* last_time); + This is useful for measuring frame time and other recurring + events. It takes the current time, returns the time difference + to the value in last_time, and stores the current time in + last_time for the next call. If the value in last_time is 0, + the return value will be zero (this usually happens on the + very first call). + + uint64_t stm_round_to_common_refresh_rate(uint64_t duration) + This oddly named function takes a measured frame time and + returns the closest "nearby" common display refresh rate frame duration + in ticks. If the input duration isn't close to any common display + refresh rate, the input duration will be returned unchanged as a fallback. + The main purpose of this function is to remove jitter/inaccuracies from + measured frame times, and instead use the display refresh rate as + frame duration. + NOTE: for more robust frame timing, consider using the + sokol_app.h function sapp_frame_duration() + + Use the following functions to convert a duration in ticks into + useful time units: + + double stm_sec(uint64_t ticks); + double stm_ms(uint64_t ticks); + double stm_us(uint64_t ticks); + double stm_ns(uint64_t ticks); + Converts a tick value into seconds, milliseconds, microseconds + or nanoseconds. Note that not all platforms will have nanosecond + or even microsecond precision. + + Uses the following time measurement functions under the hood: + + Windows: QueryPerformanceFrequency() / QueryPerformanceCounter() + MacOS/iOS: mach_absolute_time() + emscripten: emscripten_get_now() + Linux+others: clock_gettime(CLOCK_MONOTONIC) + + zlib/libpng license + + Copyright (c) 2018 Andre Weissflog + + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the + use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ +#define SOKOL_TIME_INCLUDED (1) +#include + +#if defined(SOKOL_API_DECL) && !defined(SOKOL_TIME_API_DECL) +#define SOKOL_TIME_API_DECL SOKOL_API_DECL +#endif +#ifndef SOKOL_TIME_API_DECL +#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_TIME_IMPL) +#define SOKOL_TIME_API_DECL __declspec(dllexport) +#elif defined(_WIN32) && defined(SOKOL_DLL) +#define SOKOL_TIME_API_DECL __declspec(dllimport) +#else +#define SOKOL_TIME_API_DECL extern +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +SOKOL_TIME_API_DECL void stm_setup(void); +SOKOL_TIME_API_DECL uint64_t stm_now(void); +SOKOL_TIME_API_DECL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks); +SOKOL_TIME_API_DECL uint64_t stm_since(uint64_t start_ticks); +SOKOL_TIME_API_DECL uint64_t stm_laptime(uint64_t* last_time); +SOKOL_TIME_API_DECL uint64_t stm_round_to_common_refresh_rate(uint64_t frame_ticks); +SOKOL_TIME_API_DECL double stm_sec(uint64_t ticks); +SOKOL_TIME_API_DECL double stm_ms(uint64_t ticks); +SOKOL_TIME_API_DECL double stm_us(uint64_t ticks); +SOKOL_TIME_API_DECL double stm_ns(uint64_t ticks); + +#ifdef __cplusplus +} /* extern "C" */ +#endif +#endif // SOKOL_TIME_INCLUDED + +/*-- IMPLEMENTATION ----------------------------------------------------------*/ +#ifdef SOKOL_TIME_IMPL +#define SOKOL_TIME_IMPL_INCLUDED (1) +#include /* memset */ + +#ifndef SOKOL_API_IMPL + #define SOKOL_API_IMPL +#endif +#ifndef SOKOL_ASSERT + #include + #define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE + #if defined(__GNUC__) || defined(__clang__) + #define _SOKOL_PRIVATE __attribute__((unused)) static + #else + #define _SOKOL_PRIVATE static + #endif +#endif + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +typedef struct { + uint32_t initialized; + LARGE_INTEGER freq; + LARGE_INTEGER start; +} _stm_state_t; +#elif defined(__APPLE__) && defined(__MACH__) +#include +typedef struct { + uint32_t initialized; + mach_timebase_info_data_t timebase; + uint64_t start; +} _stm_state_t; +#elif defined(__EMSCRIPTEN__) +#include +typedef struct { + uint32_t initialized; + double start; +} _stm_state_t; +#else /* anything else, this will need more care for non-Linux platforms */ +#ifdef ESP8266 +// On the ESP8266, clock_gettime ignores the first argument and CLOCK_MONOTONIC isn't defined +#define CLOCK_MONOTONIC 0 +#endif +#include +typedef struct { + uint32_t initialized; + uint64_t start; +} _stm_state_t; +#endif +static _stm_state_t _stm; + +/* prevent 64-bit overflow when computing relative timestamp + see https://gist.github.com/jspohr/3dc4f00033d79ec5bdaf67bc46c813e3 +*/ +#if defined(_WIN32) || (defined(__APPLE__) && defined(__MACH__)) +_SOKOL_PRIVATE int64_t _stm_int64_muldiv(int64_t value, int64_t numer, int64_t denom) { + int64_t q = value / denom; + int64_t r = value % denom; + return q * numer + r * numer / denom; +} +#endif + +SOKOL_API_IMPL void stm_setup(void) { + memset(&_stm, 0, sizeof(_stm)); + _stm.initialized = 0xABCDABCD; + #if defined(_WIN32) + QueryPerformanceFrequency(&_stm.freq); + QueryPerformanceCounter(&_stm.start); + #elif defined(__APPLE__) && defined(__MACH__) + mach_timebase_info(&_stm.timebase); + _stm.start = mach_absolute_time(); + #elif defined(__EMSCRIPTEN__) + _stm.start = emscripten_get_now(); + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + _stm.start = (uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec; + #endif +} + +SOKOL_API_IMPL uint64_t stm_now(void) { + SOKOL_ASSERT(_stm.initialized == 0xABCDABCD); + uint64_t now; + #if defined(_WIN32) + LARGE_INTEGER qpc_t; + QueryPerformanceCounter(&qpc_t); + now = (uint64_t) _stm_int64_muldiv(qpc_t.QuadPart - _stm.start.QuadPart, 1000000000, _stm.freq.QuadPart); + #elif defined(__APPLE__) && defined(__MACH__) + const uint64_t mach_now = mach_absolute_time() - _stm.start; + now = (uint64_t) _stm_int64_muldiv((int64_t)mach_now, (int64_t)_stm.timebase.numer, (int64_t)_stm.timebase.denom); + #elif defined(__EMSCRIPTEN__) + double js_now = emscripten_get_now() - _stm.start; + now = (uint64_t) (js_now * 1000000.0); + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + now = ((uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec) - _stm.start; + #endif + return now; +} + +SOKOL_API_IMPL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks) { + if (new_ticks > old_ticks) { + return new_ticks - old_ticks; + } + else { + return 1; + } +} + +SOKOL_API_IMPL uint64_t stm_since(uint64_t start_ticks) { + return stm_diff(stm_now(), start_ticks); +} + +SOKOL_API_IMPL uint64_t stm_laptime(uint64_t* last_time) { + SOKOL_ASSERT(last_time); + uint64_t dt = 0; + uint64_t now = stm_now(); + if (0 != *last_time) { + dt = stm_diff(now, *last_time); + } + *last_time = now; + return dt; +} + +// first number is frame duration in ns, second number is tolerance in ns, +// the resulting min/max values must not overlap! +static const uint64_t _stm_refresh_rates[][2] = { + { 16666667, 1000000 }, // 60 Hz: 16.6667 +- 1ms + { 13888889, 250000 }, // 72 Hz: 13.8889 +- 0.25ms + { 13333333, 250000 }, // 75 Hz: 13.3333 +- 0.25ms + { 11764706, 250000 }, // 85 Hz: 11.7647 +- 0.25 + { 11111111, 250000 }, // 90 Hz: 11.1111 +- 0.25ms + { 10000000, 500000 }, // 100 Hz: 10.0000 +- 0.5ms + { 8333333, 500000 }, // 120 Hz: 8.3333 +- 0.5ms + { 6944445, 500000 }, // 144 Hz: 6.9445 +- 0.5ms + { 4166667, 1000000 }, // 240 Hz: 4.1666 +- 1ms + { 0, 0 }, // keep the last element always at zero +}; + +SOKOL_API_IMPL uint64_t stm_round_to_common_refresh_rate(uint64_t ticks) { + uint64_t ns; + int i = 0; + while (0 != (ns = _stm_refresh_rates[i][0])) { + uint64_t tol = _stm_refresh_rates[i][1]; + if ((ticks > (ns - tol)) && (ticks < (ns + tol))) { + return ns; + } + i++; + } + // fallthough: didn't fit into any buckets + return ticks; +} + +SOKOL_API_IMPL double stm_sec(uint64_t ticks) { + return (double)ticks / 1000000000.0; +} + +SOKOL_API_IMPL double stm_ms(uint64_t ticks) { + return (double)ticks / 1000000.0; +} + +SOKOL_API_IMPL double stm_us(uint64_t ticks) { + return (double)ticks / 1000.0; +} + +SOKOL_API_IMPL double stm_ns(uint64_t ticks) { + return (double)ticks; +} +#endif /* SOKOL_TIME_IMPL */ + diff --git a/code/vendors/ferox/src/external/stb_ds.h b/code/vendors/ferox/src/external/stb_ds.h new file mode 100644 index 0000000..e84c82d --- /dev/null +++ b/code/vendors/ferox/src/external/stb_ds.h @@ -0,0 +1,1895 @@ +/* stb_ds.h - v0.67 - public domain data structures - Sean Barrett 2019 + + This is a single-header-file library that provides easy-to-use + dynamic arrays and hash tables for C (also works in C++). + + For a gentle introduction: + http://nothings.org/stb_ds + + To use this library, do this in *one* C or C++ file: + #define STB_DS_IMPLEMENTATION + #include "stb_ds.h" + +TABLE OF CONTENTS + + Table of Contents + Compile-time options + License + Documentation + Notes + Notes - Dynamic arrays + Notes - Hash maps + Credits + +COMPILE-TIME OPTIONS + + #define STBDS_NO_SHORT_NAMES + + This flag needs to be set globally. + + By default stb_ds exposes shorter function names that are not qualified + with the "stbds_" prefix. If these names conflict with the names in your + code, define this flag. + + #define STBDS_SIPHASH_2_4 + + This flag only needs to be set in the file containing #define STB_DS_IMPLEMENTATION. + + By default stb_ds.h hashes using a weaker variant of SipHash and a custom hash for + 4- and 8-byte keys. On 64-bit platforms, you can define the above flag to force + stb_ds.h to use specification-compliant SipHash-2-4 for all keys. Doing so makes + hash table insertion about 20% slower on 4- and 8-byte keys, 5% slower on + 64-byte keys, and 10% slower on 256-byte keys on my test computer. + + #define STBDS_REALLOC(context,ptr,size) better_realloc + #define STBDS_FREE(context,ptr) better_free + + These defines only need to be set in the file containing #define STB_DS_IMPLEMENTATION. + + By default stb_ds uses stdlib realloc() and free() for memory management. You can + substitute your own functions instead by defining these symbols. You must either + define both, or neither. Note that at the moment, 'context' will always be NULL. + @TODO add an array/hash initialization function that takes a memory context pointer. + + #define STBDS_UNIT_TESTS + + Defines a function stbds_unit_tests() that checks the functioning of the data structures. + + Note that on older versions of gcc (e.g. 5.x.x) you may need to build with '-std=c++0x' + (or equivalentally '-std=c++11') when using anonymous structures as seen on the web + page or in STBDS_UNIT_TESTS. + +LICENSE + + Placed in the public domain and also MIT licensed. + See end of file for detailed license information. + +DOCUMENTATION + + Dynamic Arrays + + Non-function interface: + + Declare an empty dynamic array of type T + T* foo = NULL; + + Access the i'th item of a dynamic array 'foo' of type T, T* foo: + foo[i] + + Functions (actually macros) + + arrfree: + void arrfree(T*); + Frees the array. + + arrlen: + ptrdiff_t arrlen(T*); + Returns the number of elements in the array. + + arrlenu: + size_t arrlenu(T*); + Returns the number of elements in the array as an unsigned type. + + arrpop: + T arrpop(T* a) + Removes the final element of the array and returns it. + + arrput: + T arrput(T* a, T b); + Appends the item b to the end of array a. Returns b. + + arrins: + T arrins(T* a, int p, T b); + Inserts the item b into the middle of array a, into a[p], + moving the rest of the array over. Returns b. + + arrinsn: + void arrinsn(T* a, int p, int n); + Inserts n uninitialized items into array a starting at a[p], + moving the rest of the array over. + + arraddnptr: + T* arraddnptr(T* a, int n) + Appends n uninitialized items onto array at the end. + Returns a pointer to the first uninitialized item added. + + arraddnindex: + size_t arraddnindex(T* a, int n) + Appends n uninitialized items onto array at the end. + Returns the index of the first uninitialized item added. + + arrdel: + void arrdel(T* a, int p); + Deletes the element at a[p], moving the rest of the array over. + + arrdeln: + void arrdeln(T* a, int p, int n); + Deletes n elements starting at a[p], moving the rest of the array over. + + arrdelswap: + void arrdelswap(T* a, int p); + Deletes the element at a[p], replacing it with the element from + the end of the array. O(1) performance. + + arrsetlen: + void arrsetlen(T* a, int n); + Changes the length of the array to n. Allocates uninitialized + slots at the end if necessary. + + arrsetcap: + size_t arrsetcap(T* a, int n); + Sets the length of allocated storage to at least n. It will not + change the length of the array. + + arrcap: + size_t arrcap(T* a); + Returns the number of total elements the array can contain without + needing to be reallocated. + + Hash maps & String hash maps + + Given T is a structure type: struct { TK key; TV value; }. Note that some + functions do not require TV value and can have other fields. For string + hash maps, TK must be 'char *'. + + Special interface: + + stbds_rand_seed: + void stbds_rand_seed(size_t seed); + For security against adversarially chosen data, you should seed the + library with a strong random number. Or at least seed it with time(). + + stbds_hash_string: + size_t stbds_hash_string(char *str, size_t seed); + Returns a hash value for a string. + + stbds_hash_bytes: + size_t stbds_hash_bytes(void *p, size_t len, size_t seed); + These functions hash an arbitrary number of bytes. The function + uses a custom hash for 4- and 8-byte data, and a weakened version + of SipHash for everything else. On 64-bit platforms you can get + specification-compliant SipHash-2-4 on all data by defining + STBDS_SIPHASH_2_4, at a significant cost in speed. + + Non-function interface: + + Declare an empty hash map of type T + T* foo = NULL; + + Access the i'th entry in a hash table T* foo: + foo[i] + + Function interface (actually macros): + + hmfree + shfree + void hmfree(T*); + void shfree(T*); + Frees the hashmap and sets the pointer to NULL. + + hmlen + shlen + ptrdiff_t hmlen(T*) + ptrdiff_t shlen(T*) + Returns the number of elements in the hashmap. + + hmlenu + shlenu + size_t hmlenu(T*) + size_t shlenu(T*) + Returns the number of elements in the hashmap. + + hmgeti + shgeti + hmgeti_ts + ptrdiff_t hmgeti(T*, TK key) + ptrdiff_t shgeti(T*, char* key) + ptrdiff_t hmgeti_ts(T*, TK key, ptrdiff_t tempvar) + Returns the index in the hashmap which has the key 'key', or -1 + if the key is not present. + + hmget + hmget_ts + shget + TV hmget(T*, TK key) + TV shget(T*, char* key) + TV hmget_ts(T*, TK key, ptrdiff_t tempvar) + Returns the value corresponding to 'key' in the hashmap. + The structure must have a 'value' field + + hmgets + shgets + T hmgets(T*, TK key) + T shgets(T*, char* key) + Returns the structure corresponding to 'key' in the hashmap. + + hmgetp + shgetp + hmgetp_ts + hmgetp_null + shgetp_null + T* hmgetp(T*, TK key) + T* shgetp(T*, char* key) + T* hmgetp_ts(T*, TK key, ptrdiff_t tempvar) + T* hmgetp_null(T*, TK key) + T* shgetp_null(T*, char *key) + Returns a pointer to the structure corresponding to 'key' in + the hashmap. Functions ending in "_null" return NULL if the key + is not present in the hashmap; the others return a pointer to a + structure holding the default value (but not the searched-for key). + + hmdefault + shdefault + TV hmdefault(T*, TV value) + TV shdefault(T*, TV value) + Sets the default value for the hashmap, the value which will be + returned by hmget/shget if the key is not present. + + hmdefaults + shdefaults + TV hmdefaults(T*, T item) + TV shdefaults(T*, T item) + Sets the default struct for the hashmap, the contents which will be + returned by hmgets/shgets if the key is not present. + + hmput + shput + TV hmput(T*, TK key, TV value) + TV shput(T*, char* key, TV value) + Inserts a pair into the hashmap. If the key is already + present in the hashmap, updates its value. + + hmputs + shputs + T hmputs(T*, T item) + T shputs(T*, T item) + Inserts a struct with T.key into the hashmap. If the struct is already + present in the hashmap, updates it. + + hmdel + shdel + int hmdel(T*, TK key) + int shdel(T*, char* key) + If 'key' is in the hashmap, deletes its entry and returns 1. + Otherwise returns 0. + + Function interface (actually macros) for strings only: + + sh_new_strdup + void sh_new_strdup(T*); + Overwrites the existing pointer with a newly allocated + string hashmap which will automatically allocate and free + each string key using realloc/free + + sh_new_arena + void sh_new_arena(T*); + Overwrites the existing pointer with a newly allocated + string hashmap which will automatically allocate each string + key to a string arena. Every string key ever used by this + hash table remains in the arena until the arena is freed. + Additionally, any key which is deleted and reinserted will + be allocated multiple times in the string arena. + +NOTES + + * These data structures are realloc'd when they grow, and the macro + "functions" write to the provided pointer. This means: (a) the pointer + must be an lvalue, and (b) the pointer to the data structure is not + stable, and you must maintain it the same as you would a realloc'd + pointer. For example, if you pass a pointer to a dynamic array to a + function which updates it, the function must return back the new + pointer to the caller. This is the price of trying to do this in C. + + * The following are the only functions that are thread-safe on a single data + structure, i.e. can be run in multiple threads simultaneously on the same + data structure + hmlen shlen + hmlenu shlenu + hmget_ts shget_ts + hmgeti_ts shgeti_ts + hmgets_ts shgets_ts + + * You iterate over the contents of a dynamic array and a hashmap in exactly + the same way, using arrlen/hmlen/shlen: + + for (i=0; i < arrlen(foo); ++i) + ... foo[i] ... + + * All operations except arrins/arrdel are O(1) amortized, but individual + operations can be slow, so these data structures may not be suitable + for real time use. Dynamic arrays double in capacity as needed, so + elements are copied an average of once. Hash tables double/halve + their size as needed, with appropriate hysteresis to maintain O(1) + performance. + +NOTES - DYNAMIC ARRAY + + * If you know how long a dynamic array is going to be in advance, you can avoid + extra memory allocations by using arrsetlen to allocate it to that length in + advance and use foo[n] while filling it out, or arrsetcap to allocate the memory + for that length and use arrput/arrpush as normal. + + * Unlike some other versions of the dynamic array, this version should + be safe to use with strict-aliasing optimizations. + +NOTES - HASH MAP + + * For compilers other than GCC and clang (e.g. Visual Studio), for hmput/hmget/hmdel + and variants, the key must be an lvalue (so the macro can take the address of it). + Extensions are used that eliminate this requirement if you're using C99 and later + in GCC or clang, or if you're using C++ in GCC. But note that this can make your + code less portable. + + * To test for presence of a key in a hashmap, just do 'hmgeti(foo,key) >= 0'. + + * The iteration order of your data in the hashmap is determined solely by the + order of insertions and deletions. In particular, if you never delete, new + keys are always added at the end of the array. This will be consistent + across all platforms and versions of the library. However, you should not + attempt to serialize the internal hash table, as the hash is not consistent + between different platforms, and may change with future versions of the library. + + * Use sh_new_arena() for string hashmaps that you never delete from. Initialize + with NULL if you're managing the memory for your strings, or your strings are + never freed (at least until the hashmap is freed). Otherwise, use sh_new_strdup(). + @TODO: make an arena variant that garbage collects the strings with a trivial + copy collector into a new arena whenever the table shrinks / rebuilds. Since + current arena recommendation is to only use arena if it never deletes, then + this can just replace current arena implementation. + + * If adversarial input is a serious concern and you're on a 64-bit platform, + enable STBDS_SIPHASH_2_4 (see the 'Compile-time options' section), and pass + a strong random number to stbds_rand_seed. + + * The default value for the hash table is stored in foo[-1], so if you + use code like 'hmget(T,k)->value = 5' you can accidentally overwrite + the value stored by hmdefault if 'k' is not present. + +CREDITS + + Sean Barrett -- library, idea for dynamic array API/implementation + Per Vognsen -- idea for hash table API/implementation + Rafael Sachetto -- arrpop() + github:HeroicKatora -- arraddn() reworking + + Bugfixes: + Andy Durdin + Shane Liesegang + Vinh Truong + Andreas Molzer + github:hashitaku + github:srdjanstipic + Macoy Madson + Andreas Vennstrom + Tobias Mansfield-Williams +*/ + +#ifdef STBDS_UNIT_TESTS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef INCLUDE_STB_DS_H +#define INCLUDE_STB_DS_H + +#include +#include + +#ifndef STBDS_NO_SHORT_NAMES +#define arrlen stbds_arrlen +#define arrlenu stbds_arrlenu +#define arrput stbds_arrput +#define arrpush stbds_arrput +#define arrpop stbds_arrpop +#define arrfree stbds_arrfree +#define arraddn stbds_arraddn // deprecated, use one of the following instead: +#define arraddnptr stbds_arraddnptr +#define arraddnindex stbds_arraddnindex +#define arrsetlen stbds_arrsetlen +#define arrlast stbds_arrlast +#define arrins stbds_arrins +#define arrinsn stbds_arrinsn +#define arrdel stbds_arrdel +#define arrdeln stbds_arrdeln +#define arrdelswap stbds_arrdelswap +#define arrcap stbds_arrcap +#define arrsetcap stbds_arrsetcap + +#define hmput stbds_hmput +#define hmputs stbds_hmputs +#define hmget stbds_hmget +#define hmget_ts stbds_hmget_ts +#define hmgets stbds_hmgets +#define hmgetp stbds_hmgetp +#define hmgetp_ts stbds_hmgetp_ts +#define hmgetp_null stbds_hmgetp_null +#define hmgeti stbds_hmgeti +#define hmgeti_ts stbds_hmgeti_ts +#define hmdel stbds_hmdel +#define hmlen stbds_hmlen +#define hmlenu stbds_hmlenu +#define hmfree stbds_hmfree +#define hmdefault stbds_hmdefault +#define hmdefaults stbds_hmdefaults + +#define shput stbds_shput +#define shputi stbds_shputi +#define shputs stbds_shputs +#define shget stbds_shget +#define shgeti stbds_shgeti +#define shgets stbds_shgets +#define shgetp stbds_shgetp +#define shgetp_null stbds_shgetp_null +#define shdel stbds_shdel +#define shlen stbds_shlen +#define shlenu stbds_shlenu +#define shfree stbds_shfree +#define shdefault stbds_shdefault +#define shdefaults stbds_shdefaults +#define sh_new_arena stbds_sh_new_arena +#define sh_new_strdup stbds_sh_new_strdup + +#define stralloc stbds_stralloc +#define strreset stbds_strreset +#endif + +#if defined(STBDS_REALLOC) && !defined(STBDS_FREE) || !defined(STBDS_REALLOC) && defined(STBDS_FREE) +#error "You must define both STBDS_REALLOC and STBDS_FREE, or neither." +#endif +#if !defined(STBDS_REALLOC) && !defined(STBDS_FREE) +#include +#define STBDS_REALLOC(c,p,s) realloc(p,s) +#define STBDS_FREE(c,p) free(p) +#endif + +#ifdef _MSC_VER +#define STBDS_NOTUSED(v) (void)(v) +#else +#define STBDS_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// for security against attackers, seed the library with a random number, at least time() but stronger is better +extern void stbds_rand_seed(size_t seed); + +// these are the hash functions used internally if you want to test them or use them for other purposes +extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed); +extern size_t stbds_hash_string(char *str, size_t seed); + +// this is a simple string arena allocator, initialize with e.g. 'stbds_string_arena my_arena={0}'. +typedef struct stbds_string_arena stbds_string_arena; +extern char * stbds_stralloc(stbds_string_arena *a, char *str); +extern void stbds_strreset(stbds_string_arena *a); + +// have to #define STBDS_UNIT_TESTS to call this +extern void stbds_unit_tests(void); + +/////////////// +// +// Everything below here is implementation details +// + +extern void * stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); +extern void stbds_arrfreef(void *a); +extern void stbds_hmfree_func(void *p, size_t elemsize); +extern void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); +extern void * stbds_hmput_default(void *a, size_t elemsize); +extern void * stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode); +extern void * stbds_shmode_func(size_t elemsize, int mode); + +#ifdef __cplusplus +} +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define STBDS_HAS_TYPEOF +#ifdef __cplusplus +//#define STBDS_HAS_LITERAL_ARRAY // this is currently broken for clang +#endif +#endif + +#if !defined(__cplusplus) +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +#define STBDS_HAS_LITERAL_ARRAY +#endif +#endif + +// this macro takes the address of the argument, but on gcc/clang can accept rvalues +#if defined(STBDS_HAS_LITERAL_ARRAY) && defined(STBDS_HAS_TYPEOF) + #if __clang__ + #define STBDS_ADDRESSOF(typevar, value) ((__typeof__(typevar)[1]){value}) // literal array decays to pointer to value + #else + #define STBDS_ADDRESSOF(typevar, value) ((typeof(typevar)[1]){value}) // literal array decays to pointer to value + #endif +#else +#define STBDS_ADDRESSOF(typevar, value) &(value) +#endif + +#define STBDS_OFFSETOF(var,field) ((char *) &(var)->field - (char *) (var)) + +#define stbds_header(t) ((stbds_array_header *) (t) - 1) +#define stbds_temp(t) stbds_header(t)->temp +#define stbds_temp_key(t) (*(char **) stbds_header(t)->hash_table) + +#define stbds_arrsetcap(a,n) (stbds_arrgrow(a,0,n)) +#define stbds_arrsetlen(a,n) ((stbds_arrcap(a) < (size_t) (n) ? stbds_arrsetcap((a),(size_t)(n)),0 : 0), (a) ? stbds_header(a)->length = (size_t) (n) : 0) +#define stbds_arrcap(a) ((a) ? stbds_header(a)->capacity : 0) +#define stbds_arrlen(a) ((a) ? (ptrdiff_t) stbds_header(a)->length : 0) +#define stbds_arrlenu(a) ((a) ? stbds_header(a)->length : 0) +#define stbds_arrput(a,v) (stbds_arrmaybegrow(a,1), (a)[stbds_header(a)->length++] = (v)) +#define stbds_arrpush stbds_arrput // synonym +#define stbds_arrpop(a) (stbds_header(a)->length--, (a)[stbds_header(a)->length]) +#define stbds_arraddn(a,n) ((void)(stbds_arraddnindex(a, n))) // deprecated, use one of the following instead: +#define stbds_arraddnptr(a,n) (stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), &(a)[stbds_header(a)->length-(n)]) : (a)) +#define stbds_arraddnindex(a,n)(stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), stbds_header(a)->length-(n)) : stbds_arrlen(a)) +#define stbds_arraddnoff stbds_arraddnindex +#define stbds_arrlast(a) ((a)[stbds_header(a)->length-1]) +#define stbds_arrfree(a) ((void) ((a) ? STBDS_FREE(NULL,stbds_header(a)) : (void)0), (a)=NULL) +#define stbds_arrdel(a,i) stbds_arrdeln(a,i,1) +#define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n)) +#define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1) +#define stbds_arrinsn(a,i,n) (stbds_arraddn((a),(n)), memmove(&(a)[(i)+(n)], &(a)[i], sizeof *(a) * (stbds_header(a)->length-(n)-(i)))) +#define stbds_arrins(a,i,v) (stbds_arrinsn((a),(i),1), (a)[i]=(v)) + +#define stbds_arrmaybegrow(a,n) ((!(a) || stbds_header(a)->length + (n) > stbds_header(a)->capacity) \ + ? (stbds_arrgrow(a,n,0),0) : 0) + +#define stbds_arrgrow(a,b,c) ((a) = stbds_arrgrowf_wrapper((a), sizeof *(a), (b), (c))) + +#define stbds_hmput(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, 0), \ + (t)[stbds_temp((t)-1)].key = (k), \ + (t)[stbds_temp((t)-1)].value = (v)) + +#define stbds_hmputs(t, s) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), &(s).key, sizeof (s).key, STBDS_HM_BINARY), \ + (t)[stbds_temp((t)-1)] = (s)) + +#define stbds_hmgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_HM_BINARY), \ + stbds_temp((t)-1)) + +#define stbds_hmgeti_ts(t,k,temp) \ + ((t) = stbds_hmget_key_ts_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, &(temp), STBDS_HM_BINARY), \ + (temp)) + +#define stbds_hmgetp(t, k) \ + ((void) stbds_hmgeti(t,k), &(t)[stbds_temp((t)-1)]) + +#define stbds_hmgetp_ts(t, k, temp) \ + ((void) stbds_hmgeti_ts(t,k,temp), &(t)[temp]) + +#define stbds_hmdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_BINARY)),(t)?stbds_temp((t)-1):0) + +#define stbds_hmdefault(t, v) \ + ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1].value = (v)) + +#define stbds_hmdefaults(t, s) \ + ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1] = (s)) + +#define stbds_hmfree(p) \ + ((void) ((p) != NULL ? stbds_hmfree_func((p)-1,sizeof*(p)),0 : 0),(p)=NULL) + +#define stbds_hmgets(t, k) (*stbds_hmgetp(t,k)) +#define stbds_hmget(t, k) (stbds_hmgetp(t,k)->value) +#define stbds_hmget_ts(t, k, temp) (stbds_hmgetp_ts(t,k,temp)->value) +#define stbds_hmlen(t) ((t) ? (ptrdiff_t) stbds_header((t)-1)->length-1 : 0) +#define stbds_hmlenu(t) ((t) ? stbds_header((t)-1)->length-1 : 0) +#define stbds_hmgetp_null(t,k) (stbds_hmgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) + +#define stbds_shput(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)].value = (v)) + +#define stbds_shputi(t, k, v) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)].value = (v), stbds_temp((t)-1)) + +#define stbds_shputs(t, s) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (s).key, sizeof (s).key, STBDS_HM_STRING), \ + (t)[stbds_temp((t)-1)] = (s), \ + (t)[stbds_temp((t)-1)].key = stbds_temp_key((t)-1)) // above line overwrites whole structure, so must rewrite key here if it was allocated internally + +#define stbds_pshput(t, p) \ + ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (p)->key, sizeof (p)->key, STBDS_HM_PTR_TO_STRING), \ + (t)[stbds_temp((t)-1)] = (p)) + +#define stbds_shgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ + stbds_temp((t)-1)) + +#define stbds_pshgeti(t,k) \ + ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_HM_PTR_TO_STRING), \ + stbds_temp((t)-1)) + +#define stbds_shgetp(t, k) \ + ((void) stbds_shgeti(t,k), &(t)[stbds_temp((t)-1)]) + +#define stbds_pshget(t, k) \ + ((void) stbds_pshgeti(t,k), (t)[stbds_temp((t)-1)]) + +#define stbds_shdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_STRING)),(t)?stbds_temp((t)-1):0) +#define stbds_pshdel(t,k) \ + (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_OFFSETOF(*(t),key), STBDS_HM_PTR_TO_STRING)),(t)?stbds_temp((t)-1):0) + +#define stbds_sh_new_arena(t) \ + ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_ARENA)) +#define stbds_sh_new_strdup(t) \ + ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_STRDUP)) + +#define stbds_shdefault(t, v) stbds_hmdefault(t,v) +#define stbds_shdefaults(t, s) stbds_hmdefaults(t,s) + +#define stbds_shfree stbds_hmfree +#define stbds_shlenu stbds_hmlenu + +#define stbds_shgets(t, k) (*stbds_shgetp(t,k)) +#define stbds_shget(t, k) (stbds_shgetp(t,k)->value) +#define stbds_shgetp_null(t,k) (stbds_shgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) +#define stbds_shlen stbds_hmlen + +typedef struct +{ + size_t length; + size_t capacity; + void * hash_table; + ptrdiff_t temp; +} stbds_array_header; + +typedef struct stbds_string_block +{ + struct stbds_string_block *next; + char storage[8]; +} stbds_string_block; + +struct stbds_string_arena +{ + stbds_string_block *storage; + size_t remaining; + unsigned char block; + unsigned char mode; // this isn't used by the string arena itself +}; + +#define STBDS_HM_BINARY 0 +#define STBDS_HM_STRING 1 + +enum +{ + STBDS_SH_NONE, + STBDS_SH_DEFAULT, + STBDS_SH_STRDUP, + STBDS_SH_ARENA +}; + +#ifdef __cplusplus +// in C we use implicit assignment from these void*-returning functions to T*. +// in C++ these templates make the same code work +template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) { + return (T*)stbds_arrgrowf((void *)a, elemsize, addlen, min_cap); +} +template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { + return (T*)stbds_hmget_key((void*)a, elemsize, key, keysize, mode); +} +template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) { + return (T*)stbds_hmget_key_ts((void*)a, elemsize, key, keysize, temp, mode); +} +template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) { + return (T*)stbds_hmput_default((void *)a, elemsize); +} +template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { + return (T*)stbds_hmput_key((void*)a, elemsize, key, keysize, mode); +} +template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){ + return (T*)stbds_hmdel_key((void*)a, elemsize, key, keysize, keyoffset, mode); +} +template static T * stbds_shmode_func_wrapper(T *, size_t elemsize, int mode) { + return (T*)stbds_shmode_func(elemsize, mode); +} +#else +#define stbds_arrgrowf_wrapper stbds_arrgrowf +#define stbds_hmget_key_wrapper stbds_hmget_key +#define stbds_hmget_key_ts_wrapper stbds_hmget_key_ts +#define stbds_hmput_default_wrapper stbds_hmput_default +#define stbds_hmput_key_wrapper stbds_hmput_key +#define stbds_hmdel_key_wrapper stbds_hmdel_key +#define stbds_shmode_func_wrapper(t,e,m) stbds_shmode_func(e,m) +#endif + +#endif // INCLUDE_STB_DS_H + + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION +// + +#ifdef STB_DS_IMPLEMENTATION +#include +#include + +#ifndef STBDS_ASSERT +#define STBDS_ASSERT_WAS_UNDEFINED +#define STBDS_ASSERT(x) ((void) 0) +#endif + +#ifdef STBDS_STATISTICS +#define STBDS_STATS(x) x +size_t stbds_array_grow; +size_t stbds_hash_grow; +size_t stbds_hash_shrink; +size_t stbds_hash_rebuild; +size_t stbds_hash_probes; +size_t stbds_hash_alloc; +size_t stbds_rehash_probes; +size_t stbds_rehash_items; +#else +#define STBDS_STATS(x) +#endif + +// +// stbds_arr implementation +// + +//int *prev_allocs[65536]; +//int num_prev; + +void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap) +{ + stbds_array_header temp={0}; // force debugging + void *b; + size_t min_len = stbds_arrlen(a) + addlen; + (void) sizeof(temp); + + // compute the minimum capacity needed + if (min_len > min_cap) + min_cap = min_len; + + if (min_cap <= stbds_arrcap(a)) + return a; + + // increase needed capacity to guarantee O(1) amortized + if (min_cap < 2 * stbds_arrcap(a)) + min_cap = 2 * stbds_arrcap(a); + else if (min_cap < 4) + min_cap = 4; + + //if (num_prev < 65536) if (a) prev_allocs[num_prev++] = (int *) ((char *) a+1); + //if (num_prev == 2201) + // num_prev = num_prev; + b = STBDS_REALLOC(NULL, (a) ? stbds_header(a) : 0, elemsize * min_cap + sizeof(stbds_array_header)); + //if (num_prev < 65536) prev_allocs[num_prev++] = (int *) (char *) b; + b = (char *) b + sizeof(stbds_array_header); + if (a == NULL) { + stbds_header(b)->length = 0; + stbds_header(b)->hash_table = 0; + stbds_header(b)->temp = 0; + } else { + STBDS_STATS(++stbds_array_grow); + } + stbds_header(b)->capacity = min_cap; + + return b; +} + +void stbds_arrfreef(void *a) +{ + STBDS_FREE(NULL, stbds_header(a)); +} + +// +// stbds_hm hash table implementation +// + +#ifdef STBDS_INTERNAL_SMALL_BUCKET +#define STBDS_BUCKET_LENGTH 4 +#else +#define STBDS_BUCKET_LENGTH 8 +#endif + +#define STBDS_BUCKET_SHIFT (STBDS_BUCKET_LENGTH == 8 ? 3 : 2) +#define STBDS_BUCKET_MASK (STBDS_BUCKET_LENGTH-1) +#define STBDS_CACHE_LINE_SIZE 64 + +#define STBDS_ALIGN_FWD(n,a) (((n) + (a) - 1) & ~((a)-1)) + +typedef struct +{ + size_t hash [STBDS_BUCKET_LENGTH]; + ptrdiff_t index[STBDS_BUCKET_LENGTH]; +} stbds_hash_bucket; // in 32-bit, this is one 64-byte cache line; in 64-bit, each array is one 64-byte cache line + +typedef struct +{ + char * temp_key; // this MUST be the first field of the hash table + size_t slot_count; + size_t used_count; + size_t used_count_threshold; + size_t used_count_shrink_threshold; + size_t tombstone_count; + size_t tombstone_count_threshold; + size_t seed; + size_t slot_count_log2; + stbds_string_arena string; + stbds_hash_bucket *storage; // not a separate allocation, just 64-byte aligned storage after this struct +} stbds_hash_index; + +#define STBDS_INDEX_EMPTY -1 +#define STBDS_INDEX_DELETED -2 +#define STBDS_INDEX_IN_USE(x) ((x) >= 0) + +#define STBDS_HASH_EMPTY 0 +#define STBDS_HASH_DELETED 1 + +static size_t stbds_hash_seed=0x31415926; + +void stbds_rand_seed(size_t seed) +{ + stbds_hash_seed = seed; +} + +#define stbds_load_32_or_64(var, temp, v32, v64_hi, v64_lo) \ + temp = v64_lo ^ v32, temp <<= 16, temp <<= 16, temp >>= 16, temp >>= 16, /* discard if 32-bit */ \ + var = v64_hi, var <<= 16, var <<= 16, /* discard if 32-bit */ \ + var ^= temp ^ v32 + +#define STBDS_SIZE_T_BITS ((sizeof (size_t)) * 8) + +static size_t stbds_probe_position(size_t hash, size_t slot_count, size_t slot_log2) +{ + size_t pos; + STBDS_NOTUSED(slot_log2); + pos = hash & (slot_count-1); + #ifdef STBDS_INTERNAL_BUCKET_START + pos &= ~STBDS_BUCKET_MASK; + #endif + return pos; +} + +static size_t stbds_log2(size_t slot_count) +{ + size_t n=0; + while (slot_count > 1) { + slot_count >>= 1; + ++n; + } + return n; +} + +static stbds_hash_index *stbds_make_hash_index(size_t slot_count, stbds_hash_index *ot) +{ + stbds_hash_index *t; + t = (stbds_hash_index *) STBDS_REALLOC(NULL,0,(slot_count >> STBDS_BUCKET_SHIFT) * sizeof(stbds_hash_bucket) + sizeof(stbds_hash_index) + STBDS_CACHE_LINE_SIZE-1); + t->storage = (stbds_hash_bucket *) STBDS_ALIGN_FWD((size_t) (t+1), STBDS_CACHE_LINE_SIZE); + t->slot_count = slot_count; + t->slot_count_log2 = stbds_log2(slot_count); + t->tombstone_count = 0; + t->used_count = 0; + + #if 0 // A1 + t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink + #elif 1 // A2 + //t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow + //t->tombstone_count_threshold = slot_count* 3/16; // if tombstones are 3/16th of table, rebuild + //t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink + + // compute without overflowing + t->used_count_threshold = slot_count - (slot_count>>2); + t->tombstone_count_threshold = (slot_count>>3) + (slot_count>>4); + t->used_count_shrink_threshold = slot_count >> 2; + + #elif 0 // B1 + t->used_count_threshold = slot_count*13/16; // if 13/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 5/16; // if table is only 5/16th full, shrink + #else // C1 + t->used_count_threshold = slot_count*14/16; // if 14/16th of table is occupied, grow + t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild + t->used_count_shrink_threshold = slot_count* 6/16; // if table is only 6/16th full, shrink + #endif + // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 + // Note that the larger tables have high variance as they were run fewer times + // A1 A2 B1 C1 + // 0.10ms : 0.10ms : 0.10ms : 0.11ms : 2,000 inserts creating 2K table + // 0.96ms : 0.95ms : 0.97ms : 1.04ms : 20,000 inserts creating 20K table + // 14.48ms : 14.46ms : 10.63ms : 11.00ms : 200,000 inserts creating 200K table + // 195.74ms : 196.35ms : 203.69ms : 214.92ms : 2,000,000 inserts creating 2M table + // 2193.88ms : 2209.22ms : 2285.54ms : 2437.17ms : 20,000,000 inserts creating 20M table + // 65.27ms : 53.77ms : 65.33ms : 65.47ms : 500,000 inserts & deletes in 2K table + // 72.78ms : 62.45ms : 71.95ms : 72.85ms : 500,000 inserts & deletes in 20K table + // 89.47ms : 77.72ms : 96.49ms : 96.75ms : 500,000 inserts & deletes in 200K table + // 97.58ms : 98.14ms : 97.18ms : 97.53ms : 500,000 inserts & deletes in 2M table + // 118.61ms : 119.62ms : 120.16ms : 118.86ms : 500,000 inserts & deletes in 20M table + // 192.11ms : 194.39ms : 196.38ms : 195.73ms : 500,000 inserts & deletes in 200M table + + if (slot_count <= STBDS_BUCKET_LENGTH) + t->used_count_shrink_threshold = 0; + // to avoid infinite loop, we need to guarantee that at least one slot is empty and will terminate probes + STBDS_ASSERT(t->used_count_threshold + t->tombstone_count_threshold < t->slot_count); + STBDS_STATS(++stbds_hash_alloc); + if (ot) { + t->string = ot->string; + // reuse old seed so we can reuse old hashes so below "copy out old data" doesn't do any hashing + t->seed = ot->seed; + } else { + size_t a,b,temp; + memset(&t->string, 0, sizeof(t->string)); + t->seed = stbds_hash_seed; + // LCG + // in 32-bit, a = 2147001325 b = 715136305 + // in 64-bit, a = 2862933555777941757 b = 3037000493 + stbds_load_32_or_64(a,temp, 2147001325, 0x27bb2ee6, 0x87b0b0fd); + stbds_load_32_or_64(b,temp, 715136305, 0, 0xb504f32d); + stbds_hash_seed = stbds_hash_seed * a + b; + } + + { + size_t i,j; + for (i=0; i < slot_count >> STBDS_BUCKET_SHIFT; ++i) { + stbds_hash_bucket *b = &t->storage[i]; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) + b->hash[j] = STBDS_HASH_EMPTY; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) + b->index[j] = STBDS_INDEX_EMPTY; + } + } + + // copy out the old data, if any + if (ot) { + size_t i,j; + t->used_count = ot->used_count; + for (i=0; i < ot->slot_count >> STBDS_BUCKET_SHIFT; ++i) { + stbds_hash_bucket *ob = &ot->storage[i]; + for (j=0; j < STBDS_BUCKET_LENGTH; ++j) { + if (STBDS_INDEX_IN_USE(ob->index[j])) { + size_t hash = ob->hash[j]; + size_t pos = stbds_probe_position(hash, t->slot_count, t->slot_count_log2); + size_t step = STBDS_BUCKET_LENGTH; + STBDS_STATS(++stbds_rehash_items); + for (;;) { + size_t limit,z; + stbds_hash_bucket *bucket; + bucket = &t->storage[pos >> STBDS_BUCKET_SHIFT]; + STBDS_STATS(++stbds_rehash_probes); + + for (z=pos & STBDS_BUCKET_MASK; z < STBDS_BUCKET_LENGTH; ++z) { + if (bucket->hash[z] == 0) { + bucket->hash[z] = hash; + bucket->index[z] = ob->index[j]; + goto done; + } + } + + limit = pos & STBDS_BUCKET_MASK; + for (z = 0; z < limit; ++z) { + if (bucket->hash[z] == 0) { + bucket->hash[z] = hash; + bucket->index[z] = ob->index[j]; + goto done; + } + } + + pos += step; // quadratic probing + step += STBDS_BUCKET_LENGTH; + pos &= (t->slot_count-1); + } + } + done: + ; + } + } + } + + return t; +} + +#define STBDS_ROTATE_LEFT(val, n) (((val) << (n)) | ((val) >> (STBDS_SIZE_T_BITS - (n)))) +#define STBDS_ROTATE_RIGHT(val, n) (((val) >> (n)) | ((val) << (STBDS_SIZE_T_BITS - (n)))) + +size_t stbds_hash_string(char *str, size_t seed) +{ + size_t hash = seed; + while (*str) + hash = STBDS_ROTATE_LEFT(hash, 9) + (unsigned char) *str++; + + // Thomas Wang 64-to-32 bit mix function, hopefully also works in 32 bits + hash ^= seed; + hash = (~hash) + (hash << 18); + hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,31); + hash = hash * 21; + hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,11); + hash += (hash << 6); + hash ^= STBDS_ROTATE_RIGHT(hash,22); + return hash+seed; +} + +#ifdef STBDS_SIPHASH_2_4 +#define STBDS_SIPHASH_C_ROUNDS 2 +#define STBDS_SIPHASH_D_ROUNDS 4 +typedef int STBDS_SIPHASH_2_4_can_only_be_used_in_64_bit_builds[sizeof(size_t) == 8 ? 1 : -1]; +#endif + +#ifndef STBDS_SIPHASH_C_ROUNDS +#define STBDS_SIPHASH_C_ROUNDS 1 +#endif +#ifndef STBDS_SIPHASH_D_ROUNDS +#define STBDS_SIPHASH_D_ROUNDS 1 +#endif + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant, for do..while(0) and sizeof()== +#endif + +static size_t stbds_siphash_bytes(void *p, size_t len, size_t seed) +{ + unsigned char *d = (unsigned char *) p; + size_t i,j; + size_t v0,v1,v2,v3, data; + + // hash that works on 32- or 64-bit registers without knowing which we have + // (computes different results on 32-bit and 64-bit platform) + // derived from siphash, but on 32-bit platforms very different as it uses 4 32-bit state not 4 64-bit + v0 = ((((size_t) 0x736f6d65 << 16) << 16) + 0x70736575) ^ seed; + v1 = ((((size_t) 0x646f7261 << 16) << 16) + 0x6e646f6d) ^ ~seed; + v2 = ((((size_t) 0x6c796765 << 16) << 16) + 0x6e657261) ^ seed; + v3 = ((((size_t) 0x74656462 << 16) << 16) + 0x79746573) ^ ~seed; + + #ifdef STBDS_TEST_SIPHASH_2_4 + // hardcoded with key material in the siphash test vectors + v0 ^= 0x0706050403020100ull ^ seed; + v1 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; + v2 ^= 0x0706050403020100ull ^ seed; + v3 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; + #endif + + #define STBDS_SIPROUND() \ + do { \ + v0 += v1; v1 = STBDS_ROTATE_LEFT(v1, 13); v1 ^= v0; v0 = STBDS_ROTATE_LEFT(v0,STBDS_SIZE_T_BITS/2); \ + v2 += v3; v3 = STBDS_ROTATE_LEFT(v3, 16); v3 ^= v2; \ + v2 += v1; v1 = STBDS_ROTATE_LEFT(v1, 17); v1 ^= v2; v2 = STBDS_ROTATE_LEFT(v2,STBDS_SIZE_T_BITS/2); \ + v0 += v3; v3 = STBDS_ROTATE_LEFT(v3, 21); v3 ^= v0; \ + } while (0) + + for (i=0; i+sizeof(size_t) <= len; i += sizeof(size_t), d += sizeof(size_t)) { + data = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + data |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // discarded if size_t == 4 + + v3 ^= data; + for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) + STBDS_SIPROUND(); + v0 ^= data; + } + data = len << (STBDS_SIZE_T_BITS-8); + switch (len - i) { + case 7: data |= ((size_t) d[6] << 24) << 24; // fall through + case 6: data |= ((size_t) d[5] << 20) << 20; // fall through + case 5: data |= ((size_t) d[4] << 16) << 16; // fall through + case 4: data |= (d[3] << 24); // fall through + case 3: data |= (d[2] << 16); // fall through + case 2: data |= (d[1] << 8); // fall through + case 1: data |= d[0]; // fall through + case 0: break; + } + v3 ^= data; + for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) + STBDS_SIPROUND(); + v0 ^= data; + v2 ^= 0xff; + for (j=0; j < STBDS_SIPHASH_D_ROUNDS; ++j) + STBDS_SIPROUND(); + +#ifdef STBDS_SIPHASH_2_4 + return v0^v1^v2^v3; +#else + return v1^v2^v3; // slightly stronger since v0^v3 in above cancels out final round operation? I tweeted at the authors of SipHash about this but they didn't reply +#endif +} + +size_t stbds_hash_bytes(void *p, size_t len, size_t seed) +{ +#ifdef STBDS_SIPHASH_2_4 + return stbds_siphash_bytes(p,len,seed); +#else + unsigned char *d = (unsigned char *) p; + + if (len == 4) { + unsigned int hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + #if 0 + // HASH32-A Bob Jenkin's hash function w/o large constants + hash ^= seed; + hash -= (hash<<6); + hash ^= (hash>>17); + hash -= (hash<<9); + hash ^= seed; + hash ^= (hash<<4); + hash -= (hash<<3); + hash ^= (hash<<10); + hash ^= (hash>>15); + #elif 1 + // HASH32-BB Bob Jenkin's presumably-accidental version of Thomas Wang hash with rotates turned into shifts. + // Note that converting these back to rotates makes it run a lot slower, presumably due to collisions, so I'm + // not really sure what's going on. + hash ^= seed; + hash = (hash ^ 61) ^ (hash >> 16); + hash = hash + (hash << 3); + hash = hash ^ (hash >> 4); + hash = hash * 0x27d4eb2d; + hash ^= seed; + hash = hash ^ (hash >> 15); + #else // HASH32-C - Murmur3 + hash ^= seed; + hash *= 0xcc9e2d51; + hash = (hash << 17) | (hash >> 15); + hash *= 0x1b873593; + hash ^= seed; + hash = (hash << 19) | (hash >> 13); + hash = hash*5 + 0xe6546b64; + hash ^= hash >> 16; + hash *= 0x85ebca6b; + hash ^= seed; + hash ^= hash >> 13; + hash *= 0xc2b2ae35; + hash ^= hash >> 16; + #endif + // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 + // Note that the larger tables have high variance as they were run fewer times + // HASH32-A // HASH32-BB // HASH32-C + // 0.10ms // 0.10ms // 0.10ms : 2,000 inserts creating 2K table + // 0.96ms // 0.95ms // 0.99ms : 20,000 inserts creating 20K table + // 14.69ms // 14.43ms // 14.97ms : 200,000 inserts creating 200K table + // 199.99ms // 195.36ms // 202.05ms : 2,000,000 inserts creating 2M table + // 2234.84ms // 2187.74ms // 2240.38ms : 20,000,000 inserts creating 20M table + // 55.68ms // 53.72ms // 57.31ms : 500,000 inserts & deletes in 2K table + // 63.43ms // 61.99ms // 65.73ms : 500,000 inserts & deletes in 20K table + // 80.04ms // 77.96ms // 81.83ms : 500,000 inserts & deletes in 200K table + // 100.42ms // 97.40ms // 102.39ms : 500,000 inserts & deletes in 2M table + // 119.71ms // 120.59ms // 121.63ms : 500,000 inserts & deletes in 20M table + // 185.28ms // 195.15ms // 187.74ms : 500,000 inserts & deletes in 200M table + // 15.58ms // 14.79ms // 15.52ms : 200,000 inserts creating 200K table with varying key spacing + + return (((size_t) hash << 16 << 16) | hash) ^ seed; + } else if (len == 8 && sizeof(size_t) == 8) { + size_t hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); + hash |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // avoid warning if size_t == 4 + hash ^= seed; + hash = (~hash) + (hash << 21); + hash ^= STBDS_ROTATE_RIGHT(hash,24); + hash *= 265; + hash ^= STBDS_ROTATE_RIGHT(hash,14); + hash ^= seed; + hash *= 21; + hash ^= STBDS_ROTATE_RIGHT(hash,28); + hash += (hash << 31); + hash = (~hash) + (hash << 18); + return hash; + } else { + return stbds_siphash_bytes(p,len,seed); + } +#endif +} +#ifdef _MSC_VER +#pragma warning(pop) +#endif + + +static int stbds_is_key_equal(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode, size_t i) +{ + if (mode >= STBDS_HM_STRING) + return 0==strcmp((char *) key, * (char **) ((char *) a + elemsize*i + keyoffset)); + else + return 0==memcmp(key, (char *) a + elemsize*i + keyoffset, keysize); +} + +#define STBDS_HASH_TO_ARR(x,elemsize) ((char*) (x) - (elemsize)) +#define STBDS_ARR_TO_HASH(x,elemsize) ((char*) (x) + (elemsize)) + +#define stbds_hash_table(a) ((stbds_hash_index *) stbds_header(a)->hash_table) + +void stbds_hmfree_func(void *a, size_t elemsize) +{ + if (a == NULL) return; + if (stbds_hash_table(a) != NULL) { + if (stbds_hash_table(a)->string.mode == STBDS_SH_STRDUP) { + size_t i; + // skip 0th element, which is default + for (i=1; i < stbds_header(a)->length; ++i) + STBDS_FREE(NULL, *(char**) ((char *) a + elemsize*i)); + } + stbds_strreset(&stbds_hash_table(a)->string); + } + STBDS_FREE(NULL, stbds_header(a)->hash_table); + STBDS_FREE(NULL, stbds_header(a)); +} + +static ptrdiff_t stbds_hm_find_slot(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) +{ + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + stbds_hash_index *table = stbds_hash_table(raw_a); + size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); + size_t step = STBDS_BUCKET_LENGTH; + size_t limit,i; + size_t pos; + stbds_hash_bucket *bucket; + + if (hash < 2) hash += 2; // stored hash values are forbidden from being 0, so we can detect empty slots + + pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); + + for (;;) { + STBDS_STATS(++stbds_hash_probes); + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + + // start searching from pos to end of bucket, this should help performance on small hash tables that fit in cache + for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + return (pos & ~STBDS_BUCKET_MASK)+i; + } + } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { + return -1; + } + } + + // search from beginning of bucket to pos + limit = pos & STBDS_BUCKET_MASK; + for (i = 0; i < limit; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + return (pos & ~STBDS_BUCKET_MASK)+i; + } + } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { + return -1; + } + } + + // quadratic probing + pos += step; + step += STBDS_BUCKET_LENGTH; + pos &= (table->slot_count-1); + } + /* NOTREACHED */ +} + +void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) +{ + size_t keyoffset = 0; + if (a == NULL) { + // make it non-empty so we can return a temp + a = stbds_arrgrowf(0, elemsize, 0, 1); + stbds_header(a)->length += 1; + memset(a, 0, elemsize); + *temp = STBDS_INDEX_EMPTY; + // adjust a to point after the default element + return STBDS_ARR_TO_HASH(a,elemsize); + } else { + stbds_hash_index *table; + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + // adjust a to point to the default element + table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; + if (table == 0) { + *temp = -1; + } else { + ptrdiff_t slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); + if (slot < 0) { + *temp = STBDS_INDEX_EMPTY; + } else { + stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + *temp = b->index[slot & STBDS_BUCKET_MASK]; + } + } + return a; + } +} + +void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) +{ + ptrdiff_t temp; + void *p = stbds_hmget_key_ts(a, elemsize, key, keysize, &temp, mode); + stbds_temp(STBDS_HASH_TO_ARR(p,elemsize)) = temp; + return p; +} + +void * stbds_hmput_default(void *a, size_t elemsize) +{ + // three cases: + // a is NULL <- allocate + // a has a hash table but no entries, because of shmode <- grow + // a has entries <- do nothing + if (a == NULL || stbds_header(STBDS_HASH_TO_ARR(a,elemsize))->length == 0) { + a = stbds_arrgrowf(a ? STBDS_HASH_TO_ARR(a,elemsize) : NULL, elemsize, 0, 1); + stbds_header(a)->length += 1; + memset(a, 0, elemsize); + a=STBDS_ARR_TO_HASH(a,elemsize); + } + return a; +} + +static char *stbds_strdup(char *str); + +void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) +{ + size_t keyoffset=0; + void *raw_a; + stbds_hash_index *table; + + if (a == NULL) { + a = stbds_arrgrowf(0, elemsize, 0, 1); + memset(a, 0, elemsize); + stbds_header(a)->length += 1; + // adjust a to point AFTER the default element + a = STBDS_ARR_TO_HASH(a,elemsize); + } + + // adjust a to point to the default element + raw_a = a; + a = STBDS_HASH_TO_ARR(a,elemsize); + + table = (stbds_hash_index *) stbds_header(a)->hash_table; + + if (table == NULL || table->used_count >= table->used_count_threshold) { + stbds_hash_index *nt; + size_t slot_count; + + slot_count = (table == NULL) ? STBDS_BUCKET_LENGTH : table->slot_count*2; + nt = stbds_make_hash_index(slot_count, table); + if (table) + STBDS_FREE(NULL, table); + else + nt->string.mode = mode >= STBDS_HM_STRING ? STBDS_SH_DEFAULT : 0; + stbds_header(a)->hash_table = table = nt; + STBDS_STATS(++stbds_hash_grow); + } + + // we iterate hash table explicitly because we want to track if we saw a tombstone + { + size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); + size_t step = STBDS_BUCKET_LENGTH; + size_t pos; + ptrdiff_t tombstone = -1; + stbds_hash_bucket *bucket; + + // stored hash values are forbidden from being 0, so we can detect empty slots to early out quickly + if (hash < 2) hash += 2; + + pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); + + for (;;) { + size_t limit, i; + STBDS_STATS(++stbds_hash_probes); + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + + // start searching from pos to end of bucket + for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + stbds_temp(a) = bucket->index[i]; + if (mode >= STBDS_HM_STRING) + stbds_temp_key(a) = * (char **) ((char *) raw_a + elemsize*bucket->index[i] + keyoffset); + return STBDS_ARR_TO_HASH(a,elemsize); + } + } else if (bucket->hash[i] == 0) { + pos = (pos & ~STBDS_BUCKET_MASK) + i; + goto found_empty_slot; + } else if (tombstone < 0) { + if (bucket->index[i] == STBDS_INDEX_DELETED) + tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); + } + } + + // search from beginning of bucket to pos + limit = pos & STBDS_BUCKET_MASK; + for (i = 0; i < limit; ++i) { + if (bucket->hash[i] == hash) { + if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { + stbds_temp(a) = bucket->index[i]; + return STBDS_ARR_TO_HASH(a,elemsize); + } + } else if (bucket->hash[i] == 0) { + pos = (pos & ~STBDS_BUCKET_MASK) + i; + goto found_empty_slot; + } else if (tombstone < 0) { + if (bucket->index[i] == STBDS_INDEX_DELETED) + tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); + } + } + + // quadratic probing + pos += step; + step += STBDS_BUCKET_LENGTH; + pos &= (table->slot_count-1); + } + found_empty_slot: + if (tombstone >= 0) { + pos = tombstone; + --table->tombstone_count; + } + ++table->used_count; + + { + ptrdiff_t i = (ptrdiff_t) stbds_arrlen(a); + // we want to do stbds_arraddn(1), but we can't use the macros since we don't have something of the right type + if ((size_t) i+1 > stbds_arrcap(a)) + *(void **) &a = stbds_arrgrowf(a, elemsize, 1, 0); + raw_a = STBDS_ARR_TO_HASH(a,elemsize); + + STBDS_ASSERT((size_t) i+1 <= stbds_arrcap(a)); + stbds_header(a)->length = i+1; + bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; + bucket->hash[pos & STBDS_BUCKET_MASK] = hash; + bucket->index[pos & STBDS_BUCKET_MASK] = i-1; + stbds_temp(a) = i-1; + + switch (table->string.mode) { + case STBDS_SH_STRDUP: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_strdup((char*) key); break; + case STBDS_SH_ARENA: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_stralloc(&table->string, (char*)key); break; + case STBDS_SH_DEFAULT: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = (char *) key; break; + default: memcpy((char *) a + elemsize*i, key, keysize); break; + } + } + return STBDS_ARR_TO_HASH(a,elemsize); + } +} + +void * stbds_shmode_func(size_t elemsize, int mode) +{ + void *a = stbds_arrgrowf(0, elemsize, 0, 1); + stbds_hash_index *h; + memset(a, 0, elemsize); + stbds_header(a)->length = 1; + stbds_header(a)->hash_table = h = (stbds_hash_index *) stbds_make_hash_index(STBDS_BUCKET_LENGTH, NULL); + h->string.mode = (unsigned char) mode; + return STBDS_ARR_TO_HASH(a,elemsize); +} + +void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) +{ + if (a == NULL) { + return 0; + } else { + stbds_hash_index *table; + void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); + table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; + stbds_temp(raw_a) = 0; + if (table == 0) { + return a; + } else { + ptrdiff_t slot; + slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); + if (slot < 0) + return a; + else { + stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + int i = slot & STBDS_BUCKET_MASK; + ptrdiff_t old_index = b->index[i]; + ptrdiff_t final_index = (ptrdiff_t) stbds_arrlen(raw_a)-1-1; // minus one for the raw_a vs a, and minus one for 'last' + STBDS_ASSERT(slot < (ptrdiff_t) table->slot_count); + --table->used_count; + ++table->tombstone_count; + stbds_temp(raw_a) = 1; + STBDS_ASSERT(table->used_count >= 0); + //STBDS_ASSERT(table->tombstone_count < table->slot_count/4); + b->hash[i] = STBDS_HASH_DELETED; + b->index[i] = STBDS_INDEX_DELETED; + + if (mode == STBDS_HM_STRING && table->string.mode == STBDS_SH_STRDUP) + STBDS_FREE(NULL, *(char**) ((char *) a+elemsize*old_index)); + + // if indices are the same, memcpy is a no-op, but back-pointer-fixup will fail, so skip + if (old_index != final_index) { + // swap delete + memmove((char*) a + elemsize*old_index, (char*) a + elemsize*final_index, elemsize); + + // now find the slot for the last element + if (mode == STBDS_HM_STRING) + slot = stbds_hm_find_slot(a, elemsize, *(char**) ((char *) a+elemsize*old_index + keyoffset), keysize, keyoffset, mode); + else + slot = stbds_hm_find_slot(a, elemsize, (char* ) a+elemsize*old_index + keyoffset, keysize, keyoffset, mode); + STBDS_ASSERT(slot >= 0); + b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; + i = slot & STBDS_BUCKET_MASK; + STBDS_ASSERT(b->index[i] == final_index); + b->index[i] = old_index; + } + stbds_header(raw_a)->length -= 1; + + if (table->used_count < table->used_count_shrink_threshold && table->slot_count > STBDS_BUCKET_LENGTH) { + stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count>>1, table); + STBDS_FREE(NULL, table); + STBDS_STATS(++stbds_hash_shrink); + } else if (table->tombstone_count > table->tombstone_count_threshold) { + stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count , table); + STBDS_FREE(NULL, table); + STBDS_STATS(++stbds_hash_rebuild); + } + + return a; + } + } + } + /* NOTREACHED */ +} + +static char *stbds_strdup(char *str) +{ + // to keep replaceable allocator simple, we don't want to use strdup. + // rolling our own also avoids problem of strdup vs _strdup + size_t len = strlen(str)+1; + char *p = (char*) STBDS_REALLOC(NULL, 0, len); + memmove(p, str, len); + return p; +} + +#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MIN +#define STBDS_STRING_ARENA_BLOCKSIZE_MIN 512u +#endif +#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MAX +#define STBDS_STRING_ARENA_BLOCKSIZE_MAX (1u<<20) +#endif + +char *stbds_stralloc(stbds_string_arena *a, char *str) +{ + char *p; + size_t len = strlen(str)+1; + if (len > a->remaining) { + // compute the next blocksize + size_t blocksize = a->block; + + // size is 512, 512, 1024, 1024, 2048, 2048, 4096, 4096, etc., so that + // there are log(SIZE) allocations to free when we destroy the table + blocksize = (size_t) (STBDS_STRING_ARENA_BLOCKSIZE_MIN) << (blocksize>>1); + + // if size is under 1M, advance to next blocktype + if (blocksize < (size_t)(STBDS_STRING_ARENA_BLOCKSIZE_MAX)) + ++a->block; + + if (len > blocksize) { + // if string is larger than blocksize, then just allocate the full size. + // note that we still advance string_block so block size will continue + // increasing, so e.g. if somebody only calls this with 1000-long strings, + // eventually the arena will start doubling and handling those as well + stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + len); + memmove(sb->storage, str, len); + if (a->storage) { + // insert it after the first element, so that we don't waste the space there + sb->next = a->storage->next; + a->storage->next = sb; + } else { + sb->next = 0; + a->storage = sb; + a->remaining = 0; // this is redundant, but good for clarity + } + return sb->storage; + } else { + stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + blocksize); + sb->next = a->storage; + a->storage = sb; + a->remaining = blocksize; + } + } + + STBDS_ASSERT(len <= a->remaining); + p = a->storage->storage + a->remaining - len; + a->remaining -= len; + memmove(p, str, len); + return p; +} + +void stbds_strreset(stbds_string_arena *a) +{ + stbds_string_block *x,*y; + x = a->storage; + while (x) { + y = x->next; + STBDS_FREE(NULL, x); + x = y; + } + memset(a, 0, sizeof(*a)); +} + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// UNIT TESTS +// + +#ifdef STBDS_UNIT_TESTS +#include +#ifdef STBDS_ASSERT_WAS_UNDEFINED +#undef STBDS_ASSERT +#endif +#ifndef STBDS_ASSERT +#define STBDS_ASSERT assert +#include +#endif + +typedef struct { int key,b,c,d; } stbds_struct; +typedef struct { int key[2],b,c,d; } stbds_struct2; + +static char buffer[256]; +char *strkey(int n) +{ +#if defined(_WIN32) && defined(__STDC_WANT_SECURE_LIB__) + sprintf_s(buffer, sizeof(buffer), "test_%d", n); +#else + sprintf(buffer, "test_%d", n); +#endif + return buffer; +} + +void stbds_unit_tests(void) +{ +#if defined(_MSC_VER) && _MSC_VER <= 1200 && defined(__cplusplus) + // VC6 C++ doesn't like the template<> trick on unnamed structures, so do nothing! + STBDS_ASSERT(0); +#else + const int testsize = 100000; + const int testsize2 = testsize/20; + int *arr=NULL; + struct { int key; int value; } *intmap = NULL; + struct { char *key; int value; } *strmap = NULL, s; + struct { stbds_struct key; int value; } *map = NULL; + stbds_struct *map2 = NULL; + stbds_struct2 *map3 = NULL; + stbds_string_arena sa = { 0 }; + int key3[2] = { 1,2 }; + ptrdiff_t temp; + + int i,j; + + STBDS_ASSERT(arrlen(arr)==0); + for (i=0; i < 20000; i += 50) { + for (j=0; j < i; ++j) + arrpush(arr,j); + arrfree(arr); + } + + for (i=0; i < 4; ++i) { + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + arrdel(arr,i); + arrfree(arr); + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + arrdelswap(arr,i); + arrfree(arr); + } + + for (i=0; i < 5; ++i) { + arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); + stbds_arrins(arr,i,5); + STBDS_ASSERT(arr[i] == 5); + if (i < 4) + STBDS_ASSERT(arr[4] == 4); + arrfree(arr); + } + + i = 1; + STBDS_ASSERT(hmgeti(intmap,i) == -1); + hmdefault(intmap, -2); + STBDS_ASSERT(hmgeti(intmap, i) == -1); + STBDS_ASSERT(hmget (intmap, i) == -2); + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*5); + for (i=0; i < testsize; i+=1) { + if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*5); + if (i & 1) STBDS_ASSERT(hmget_ts(intmap, i, temp) == -2 ); + else STBDS_ASSERT(hmget_ts(intmap, i, temp) == i*5); + } + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*3); + for (i=0; i < testsize; i+=1) + if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*3); + for (i=2; i < testsize; i+=4) + hmdel(intmap, i); // delete half the entries + for (i=0; i < testsize; i+=1) + if (i & 3) STBDS_ASSERT(hmget(intmap, i) == -2 ); + else STBDS_ASSERT(hmget(intmap, i) == i*3); + for (i=0; i < testsize; i+=1) + hmdel(intmap, i); // delete the rest of the entries + for (i=0; i < testsize; i+=1) + STBDS_ASSERT(hmget(intmap, i) == -2 ); + hmfree(intmap); + for (i=0; i < testsize; i+=2) + hmput(intmap, i, i*3); + hmfree(intmap); + + #if defined(__clang__) || defined(__GNUC__) + #ifndef __cplusplus + intmap = NULL; + hmput(intmap, 15, 7); + hmput(intmap, 11, 3); + hmput(intmap, 9, 5); + STBDS_ASSERT(hmget(intmap, 9) == 5); + STBDS_ASSERT(hmget(intmap, 11) == 3); + STBDS_ASSERT(hmget(intmap, 15) == 7); + #endif + #endif + + for (i=0; i < testsize; ++i) + stralloc(&sa, strkey(i)); + strreset(&sa); + + { + s.key = "a", s.value = 1; + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key == s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + { + s.key = "a", s.value = 1; + sh_new_strdup(strmap); + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key != s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + { + s.key = "a", s.value = 1; + sh_new_arena(strmap); + shputs(strmap, s); + STBDS_ASSERT(*strmap[0].key == 'a'); + STBDS_ASSERT(strmap[0].key != s.key); + STBDS_ASSERT(strmap[0].value == s.value); + shfree(strmap); + } + + for (j=0; j < 2; ++j) { + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + if (j == 0) + sh_new_strdup(strmap); + else + sh_new_arena(strmap); + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + shdefault(strmap, -2); + STBDS_ASSERT(shgeti(strmap,"foo") == -1); + for (i=0; i < testsize; i+=2) + shput(strmap, strkey(i), i*3); + for (i=0; i < testsize; i+=1) + if (i & 1) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); + for (i=2; i < testsize; i+=4) + shdel(strmap, strkey(i)); // delete half the entries + for (i=0; i < testsize; i+=1) + if (i & 3) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); + for (i=0; i < testsize; i+=1) + shdel(strmap, strkey(i)); // delete the rest of the entries + for (i=0; i < testsize; i+=1) + STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); + shfree(strmap); + } + + { + struct { char *key; char value; } *hash = NULL; + char name[4] = "jen"; + shput(hash, "bob" , 'h'); + shput(hash, "sally" , 'e'); + shput(hash, "fred" , 'l'); + shput(hash, "jen" , 'x'); + shput(hash, "doug" , 'o'); + + shput(hash, name , 'l'); + shfree(hash); + } + + for (i=0; i < testsize; i += 2) { + stbds_struct s = { i,i*2,i*3,i*4 }; + hmput(map, s, i*5); + } + + for (i=0; i < testsize; i += 1) { + stbds_struct s = { i,i*2,i*3 ,i*4 }; + stbds_struct t = { i,i*2,i*3+1,i*4 }; + if (i & 1) STBDS_ASSERT(hmget(map, s) == 0); + else STBDS_ASSERT(hmget(map, s) == i*5); + if (i & 1) STBDS_ASSERT(hmget_ts(map, s, temp) == 0); + else STBDS_ASSERT(hmget_ts(map, s, temp) == i*5); + //STBDS_ASSERT(hmget(map, t.key) == 0); + } + + for (i=0; i < testsize; i += 2) { + stbds_struct s = { i,i*2,i*3,i*4 }; + hmputs(map2, s); + } + hmfree(map); + + for (i=0; i < testsize; i += 1) { + stbds_struct s = { i,i*2,i*3,i*4 }; + stbds_struct t = { i,i*2,i*3+1,i*4 }; + if (i & 1) STBDS_ASSERT(hmgets(map2, s.key).d == 0); + else STBDS_ASSERT(hmgets(map2, s.key).d == i*4); + //STBDS_ASSERT(hmgetp(map2, t.key) == 0); + } + hmfree(map2); + + for (i=0; i < testsize; i += 2) { + stbds_struct2 s = { { i,i*2 }, i*3,i*4, i*5 }; + hmputs(map3, s); + } + for (i=0; i < testsize; i += 1) { + stbds_struct2 s = { { i,i*2}, i*3, i*4, i*5 }; + stbds_struct2 t = { { i,i*2}, i*3+1, i*4, i*5 }; + if (i & 1) STBDS_ASSERT(hmgets(map3, s.key).d == 0); + else STBDS_ASSERT(hmgets(map3, s.key).d == i*5); + //STBDS_ASSERT(hmgetp(map3, t.key) == 0); + } +#endif +} +#endif + + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2019 Sean Barrett +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. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +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 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. +------------------------------------------------------------------------------ +*/ diff --git a/code/vendors/ferox/src/geometry.c b/code/vendors/ferox/src/geometry.c new file mode 100644 index 0000000..b940f43 --- /dev/null +++ b/code/vendors/ferox/src/geometry.c @@ -0,0 +1,474 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +/* | `geometry` 모듈 자료형 정의... | */ + +/* 충돌 감지용 도형을 나타내는 구조체. */ +typedef struct frShape { + frShapeType type; + frMaterial material; + bool isRect; + float area; + union { + struct { + float radius; + } circle; + struct { + frVertices vertices; + frVertices normals; + } polygon; + }; +} frShape; + +/* | `geometry` 모듈 상수... | */ + +static const float INVERSE_TWELVE = (1.0f / 12.0f); + +/* | `geometry` 모듈 함수... | */ + +/* 도형 `s`의 넓이를 계산한다. */ +static void frComputeArea(frShape *s); + +/* 다각형 `s`를 볼록 다각형으로 변형한다. */ +static void frComputeConvex(frShape *s); + +/* 꼭짓점 배열 `vertices`로 만들 수 있는 가장 큰 볼록 다각형의 꼭짓점 배열을 반환한다. */ +static frVertices frJarvisMarch(frVertices *vertices); + +/* 반지름이 `radius`인 원을 나타내는 도형을 생성한다. */ +frShape *frCreateCircle(frMaterial material, float radius) { + frShape *result = frCreateShape(); + + result->type = FR_SHAPE_CIRCLE; + result->material = material; + + frSetCircleRadius(result, radius); + + return result; +} + +/* 가로 길이가 `width`이고 세로 길이가 `height`인 직사각형을 나타내는 도형을 생성한다. */ +frShape *frCreateRectangle(frMaterial material, float width, float height) { + frShape *result = frCreateShape(); + + const float halfWidth = 0.5f * width, halfHeight = 0.5f * height; + + frVertices vertices = { + .data = { + (Vector2) { -halfWidth, -halfHeight }, + (Vector2) { -halfWidth, halfHeight }, + (Vector2) { halfWidth, halfHeight }, + (Vector2) { halfWidth, -halfHeight } + }, + .count = 4 + }; + + result->type = FR_SHAPE_POLYGON; + result->material = material; + result->isRect = true; + + frSetPolygonVertices(result, vertices); + + return result; +} + +/* 꼭짓점 배열이 `vertices`인 다각형을 나타내는 도형을 생성한다. */ +frShape *frCreatePolygon(frMaterial material, frVertices vertices) { + frShape *result = frCreateShape(); + + if (vertices.count > FR_GEOMETRY_MAX_VERTEX_COUNT) + vertices.count = FR_GEOMETRY_MAX_VERTEX_COUNT; + + result->type = FR_SHAPE_POLYGON; + result->material = material; + + frSetPolygonVertices(result, vertices); + + return result; +} + +/* 형태가 정해지지 않은 도형을 생성한다. */ +frShape *frCreateShape(void) { + frShape *result = calloc(1, sizeof(*result)); + + result->type = FR_SHAPE_UNKNOWN; + + return result; +} + +/* 도형 `s`와 형태가 같은 새로운 도형을 반환한다. */ +frShape *frCloneShape(frShape *s) { + if (s == NULL || s->type == FR_SHAPE_UNKNOWN) return NULL; + + frShape *result = frCreateShape(); + + result->type = s->type; + result->material = s->material; + result->isRect = s->isRect; + result->area = s->area; + + if (result->type == FR_SHAPE_CIRCLE) + result->circle.radius = s->circle.radius; + else if (result->type == FR_SHAPE_POLYGON) { + result->polygon.vertices.count = s->polygon.vertices.count; + result->polygon.normals.count = s->polygon.normals.count; + + for (int i = 0; i < s->polygon.vertices.count; i++) + result->polygon.vertices.data[i] = s->polygon.vertices.data[i]; + + for (int i = 0; i < s->polygon.normals.count; i++) + result->polygon.normals.data[i] = s->polygon.normals.data[i]; + } + + return result; +} + +/* 도형 `s`에 할당된 메모리를 해제한다. */ +void frReleaseShape(frShape *s) { + free(s); +} + +/* 구조체 `frShape`의 크기를 반환한다. */ +size_t frGetShapeStructSize(void) { + return sizeof(frShape); +} + +/* 도형 `s`의 종류를 반환한다. */ +frShapeType frGetShapeType(frShape *s) { + return (s != NULL) ? s->type : FR_SHAPE_UNKNOWN; +} + +/* 도형 `s`의 재질을 반환한다. */ +frMaterial frGetShapeMaterial(frShape *s) { + return (s != NULL) ? s->material : FR_STRUCT_ZERO(frMaterial); +} + +/* 도형 `s`의 넓이를 반환한다. */ +float frGetShapeArea(frShape *s) { + return (s != NULL && s->area >= 0.0f) ? s->area : 0.0f; +} + +/* 도형 `s`의 질량을 반환한다. */ +float frGetShapeMass(frShape *s) { + return (s != NULL) ? s->material.density * frGetShapeArea(s) : 0.0f; +} + +/* 도형 `s`의 관성 모멘트를 반환한다. */ +float frGetShapeInertia(frShape *s) { + if (s == NULL || s->type == FR_SHAPE_UNKNOWN) return 0.0f; + + float result = 0.0f; + + if (s->type == FR_SHAPE_CIRCLE) { + result = 0.5f * frGetShapeMass(s) * (s->circle.radius * s->circle.radius); + } else if (s->type == FR_SHAPE_POLYGON) { + float xInertia = 0.0f, yInertia = 0.0f; + + // https://en.wikipedia.org/wiki/Second_moment_of_area#Any_polygon + for (int j = s->polygon.vertices.count - 1, i = 0; i < s->polygon.vertices.count; j = i, i++) { + Vector2 v1 = s->polygon.vertices.data[j]; + Vector2 v2 = s->polygon.vertices.data[i]; + + const float cross = frVec2CrossProduct(v1, v2); + + xInertia += cross * ((v1.y * v1.y) + (v1.y * v2.y) + (v2.y * v2.y)); + yInertia += cross * ((v1.x * v1.x) + (v1.x * v2.x) + (v2.x * v2.x)); + } + + result = fabsf((xInertia + yInertia) * INVERSE_TWELVE); + } + + return s->material.density * result; +} + +/* 도형 `s`의 AABB를 반환한다. */ +Rectangle frGetShapeAABB(frShape *s, frTransform tx) { + Rectangle result = FR_STRUCT_ZERO(Rectangle); + + if (s == NULL || s->type == FR_SHAPE_UNKNOWN) return result; + + if (s->type == FR_SHAPE_CIRCLE) { + result.x = tx.position.x - s->circle.radius; + result.y = tx.position.y - s->circle.radius; + + result.width = result.height = 2.0f * s->circle.radius; + + return result; + } else if (s->type == FR_SHAPE_POLYGON) { + Vector2 minVertex = { FLT_MAX, FLT_MAX }; + Vector2 maxVertex = { -FLT_MAX, -FLT_MAX }; + + // AABB의 X좌표와 Y좌표의 최솟값과 최댓값을 구한다. + for (int i = 0; i < s->polygon.vertices.count; i++) { + Vector2 vertex = frVec2Transform(s->polygon.vertices.data[i], tx); + + if (minVertex.x > vertex.x) minVertex.x = vertex.x; + if (minVertex.y > vertex.y) minVertex.y = vertex.y; + + if (maxVertex.x < vertex.x) maxVertex.x = vertex.x; + if (maxVertex.y < vertex.y) maxVertex.y = vertex.y; + } + + float deltaX = maxVertex.x - minVertex.x; + float deltaY = maxVertex.y - minVertex.y; + + result.x = minVertex.x; + result.y = minVertex.y; + + result.width = deltaX; + result.height = deltaY; + + return result; + } +} + +/* 원 `s`의 반지름을 반환한다. */ +float frGetCircleRadius(frShape *s) { + return (s != NULL && s->type == FR_SHAPE_CIRCLE) ? s->circle.radius : 0.0f; +} + +/* 직사각형 `s`의 가로 및 세로 길이를 반환한다. */ +Vector2 frGetRectangleDimensions(frShape *s) { + if (!frIsShapeRectangle(s)) return FR_STRUCT_ZERO(Vector2); + + return (Vector2) { + s->polygon.vertices.data[2].x - s->polygon.vertices.data[1].x, + s->polygon.vertices.data[1].y - s->polygon.vertices.data[0].y + }; +} + +/* 다각형 `s`의 `i + 1`번째 꼭짓점을 반환한다. */ +Vector2 frGetPolygonVertex(frShape *s, int i) { + return (s != NULL && i >= 0 && i < s->polygon.vertices.count) + ? s->polygon.vertices.data[i] + : FR_STRUCT_ZERO(Vector2); +} + +/* 다각형 `s`의 `i + 1`번째 법선 벡터를 반환한다. */ +Vector2 frGetPolygonNormal(frShape *s, int i) { + return (s != NULL && i >= 0 && i < s->polygon.normals.count) + ? s->polygon.normals.data[i] + : FR_STRUCT_ZERO(Vector2); +} + +/* 다각형 `s`의 꼭짓점 배열을 반환한다. */ +frVertices frGetPolygonVertices(frShape *s) { + return (s != NULL) ? s->polygon.vertices : FR_STRUCT_ZERO(frVertices); +} + +/* 다각형 `s`의 법선 벡터 배열을 반환한다. */ +frVertices frGetPolygonNormals(frShape *s) { + return (s != NULL) ? s->polygon.normals : FR_STRUCT_ZERO(frVertices); +} + +/* 다각형 `s`가 직사각형인지 확인한다. */ +bool frIsShapeRectangle(frShape *s) { + return (s != NULL) && (s->type == FR_SHAPE_POLYGON) && s->isRect; +} + +/* 원 `s`의 반지름을 `radius`로 변경한다. */ +void frSetCircleRadius(frShape *s, float radius) { + if (s == NULL || s->type != FR_SHAPE_CIRCLE) return; + + s->circle.radius = radius; + + frComputeArea(s); +} + +/* 직사각형 `s`의 가로 및 세로 길이를 `v`의 X값과 Y값으로 설정한다. */ +void frSetRectangleDimensions(frShape *s, Vector2 v) { + if (!frIsShapeRectangle(s) || v.x <= 0.0f || v.y <= 0.0f) return; + + v = frVec2ScalarMultiply(v, 0.5f); + + frVertices vertices = { + .data = { + (Vector2) { -v.x, -v.y }, + (Vector2) { -v.x, v.y }, + (Vector2) { v.x, v.y }, + (Vector2) { v.x, -v.y } + }, + .count = 4 + }; + + frSetPolygonVertices(s, vertices); +} + +/* 다각형 `s`의 꼭짓점 배열을 `vertices`로 변경한다. */ +void frSetPolygonVertices(frShape *s, frVertices vertices) { + if (s == NULL || s->type != FR_SHAPE_POLYGON) return; + + s->polygon.vertices.count = vertices.count; + s->polygon.normals.count = vertices.count; + + for (int i = 0; i < vertices.count; i++) + s->polygon.vertices.data[i] = vertices.data[i]; + + // 직사각형은 이미 볼록 다각형이므로 변형하지 않는다. + if (!s->isRect) frComputeConvex(s); + + frComputeArea(s); + + for (int j = vertices.count - 1, i = 0; i < vertices.count; j = i, i++) + s->polygon.normals.data[i] = frVec2LeftNormal( + frVec2Subtract( + s->polygon.vertices.data[i], + s->polygon.vertices.data[j] + ) + ); +} + +/* 도형 `s`의 재질을 `material`로 설정한다. */ +void frSetShapeMaterial(frShape *s, frMaterial material) { + if (s != NULL) s->material = material; +} + +/* 도형 `s`의 종류를 `type`으로 변경한다. */ +void frSetShapeType(frShape *s, frShapeType type) { + if (s != NULL && s->type != type) s->type = type; +} + +/* 점 `p`가 도형 `s`의 내부에 있는지 확인한다. */ +bool frShapeContainsPoint(frShape *s, frTransform tx, Vector2 p) { + if (s == NULL || s->type == FR_SHAPE_UNKNOWN) return false; + + Rectangle aabb = frGetShapeAABB(s, tx); + + // `p`가 `s`의 AABB 내부에 있는지 먼저 확인한다. + if (p.x < aabb.x || p.x > aabb.x + aabb.width + || p.y < aabb.y || p.y > aabb.y + aabb.height) + return false; + + if (s->type == FR_SHAPE_CIRCLE) { + float deltaX = p.x - tx.position.x; + float deltaY = p.y - tx.position.y; + + float radius = frGetCircleRadius(s); + + return (deltaX * deltaX) + (deltaY * deltaY) <= radius * radius; + } else if (s->type == FR_SHAPE_POLYGON) { + frRay ray = { p, (Vector2) { .x = 1.0f }, FLT_MAX }; + + frRaycastHit raycast = frComputeShapeRaycast(s, tx, ray); + + return raycast.inside; + } +} + +/* 도형 `s`의 넓이를 계산한다. */ +static void frComputeArea(frShape *s) { + if (s == NULL || s->type == FR_SHAPE_UNKNOWN) + s->area = 0.0f; + else if (s->type == FR_SHAPE_CIRCLE) + s->area = PI * (s->circle.radius * s->circle.radius); + else if (s->type == FR_SHAPE_POLYGON) { + if (s->isRect) { + frVertices *vertices = &(s->polygon.vertices); + + float width = vertices->data[2].x - vertices->data[1].x; + float height = vertices->data[1].y - vertices->data[0].y; + + s->area = width * height; + + return; + } + + float twiceAreaSum = 0.0f; + + for (int i = 0; i < s->polygon.vertices.count - 1; i++) { + float twiceArea = frVec2CrossProduct( + frVec2Subtract(s->polygon.vertices.data[i], s->polygon.vertices.data[0]), + frVec2Subtract(s->polygon.vertices.data[i + 1], s->polygon.vertices.data[0]) + ); + + twiceAreaSum += twiceArea; + } + + s->area = fabsf(0.5f * twiceAreaSum); + } +} + +/* 다각형 `s`를 볼록 다각형으로 변형한다. */ +static void frComputeConvex(frShape *s) { + if (s == NULL || s->type != FR_SHAPE_POLYGON) return; + + frVertices result = frJarvisMarch(&(s->polygon.vertices)); + + for (int i = 0; i < result.count; i++) + s->polygon.vertices.data[i] = result.data[i]; + + s->polygon.vertices.count = result.count; +} + +/* 꼭짓점 배열 `vertices`로 만들 수 있는 가장 큰 볼록 다각형의 꼭짓점 배열을 반환한다. */ +static frVertices frJarvisMarch(frVertices *vertices) { + if (vertices == NULL || vertices->count == 0) return FR_STRUCT_ZERO(frVertices); + + frVertices result = FR_STRUCT_ZERO(frVertices); + + int leftmostIndex = 0, pivotIndex = 0, nextIndex = 0, vertexCount = 0; + + // 주어진 꼭짓점 배열에서 X좌표 값이 가장 작은 꼭짓점 L을 찾는다. + for (int i = 1; i < vertices->count; i++) + if (vertices->data[leftmostIndex].x > vertices->data[i].x) + leftmostIndex = i; + + result.data[vertexCount++] = vertices->data[leftmostIndex]; + + // 기준점 P를 방금 찾은 꼭짓점 L로 설정한다. + pivotIndex = leftmostIndex; + + while (1) { + // 기준점 P의 바로 다음에 오는 꼭짓점 Q를 찾는다. + for (int i = 0; i < vertices->count; i++) { + if (i == pivotIndex) continue; + + nextIndex = i; + + break; + } + + // 기준점 P와 꼭짓점 Q 사이에 오는 꼭짓점 R을 찾는다. + for (int i = 0; i < vertices->count; i++) { + if (i == pivotIndex || i == nextIndex) + continue; + + // 기준점 P, 꼭짓점 R과 꼭짓점 Q가 반시계 방향으로 정렬되어 있는지 확인한다. + if (frVec2CCW(vertices->data[pivotIndex], vertices->data[i], vertices->data[nextIndex])) + nextIndex = i; + } + + // 꼭짓점 Q가 시작점인 꼭짓점 L이 되면 탐색을 종료한다. + if (nextIndex == leftmostIndex) break; + + pivotIndex = nextIndex; + + // 새로 찾은 꼭짓점을 배열에 저장한다. + result.data[vertexCount++] = vertices->data[nextIndex]; + } + + result.count = vertexCount; + + return result; +} diff --git a/code/vendors/ferox/src/timer.c b/code/vendors/ferox/src/timer.c new file mode 100644 index 0000000..a7c312f --- /dev/null +++ b/code/vendors/ferox/src/timer.c @@ -0,0 +1,65 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +/* | `timer` 모듈 매크로 정의... | */ + +#if defined(_WIN32) + #define NOGDI + #define NOUSER +#endif + +#define SOKOL_TIME_IMPL +#include "sokol_time.h" + +/* | `timer` 모듈 변수... | */ + +/* 단조 시계의 초기화 여부를 나타내는 변수. */ +static bool initialized = false; + +/* | `timer` 모듈 함수... | */ + +/* 단조 시계를 초기화한다. */ +void frInitClock(void) { + if (!initialized) { + initialized = true; + stm_setup(); + } +} + +/* 단조 시계의 현재 시각 (단위: ms)을 반환한다. */ +double frGetCurrentTime(void) { + if (!initialized) frInitClock(); + + return stm_ms(stm_now()); +} + +/* 단조 시계의 시각 `newTime`과 `oldTime`의 차이를 반환한다. */ +double frGetTimeDifference(double newTime, double oldTime) { + return newTime - oldTime; +} + +/* 단조 시계의 현재 시각과 `old_time`과의 차이를 반환한다. */ +double frGetTimeSince(double oldTime) { + return frGetTimeDifference(frGetCurrentTime(), oldTime); +} diff --git a/code/vendors/ferox/src/utils.c b/code/vendors/ferox/src/utils.c new file mode 100644 index 0000000..1d27918 --- /dev/null +++ b/code/vendors/ferox/src/utils.c @@ -0,0 +1,48 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" + +#define STB_DS_IMPLEMENTATION +#include "stb_ds.h" + +/* | `utils` 모듈 매크로 정의... | */ + +#define TWO_PI (2.0f * PI) + +/* | `utils` 모듈 상수... | */ + +static const float INVERSE_TWO_PI = (1.0f / TWO_PI); + +/* | `utils` 모듈 함수... | */ + +/* 각도 `angle` (단위: rad.)을 정규화하여, 구간 `[center - π, center + π]`에 포함되도록 한다. */ +float frNormalizeAngle(float angle, float center) { + if (angle > center - PI && angle < center + PI) return angle; + + return angle - (TWO_PI * floorf((angle + PI - center) * INVERSE_TWO_PI)); +} + +/* 부동 소수점 값 `f1`이 `f2`와 근접한 값인지 확인한다. */ +bool frNumberApproxEquals(float f1, float f2) { + return fabsf(f1 - f2) <= fmaxf(1.0f, fmaxf(f1, f2)) * FLT_EPSILON; +} \ No newline at end of file diff --git a/code/vendors/ferox/src/world.c b/code/vendors/ferox/src/world.c new file mode 100644 index 0000000..6fffd67 --- /dev/null +++ b/code/vendors/ferox/src/world.c @@ -0,0 +1,377 @@ +/* + Copyright (c) 2021-2022 jdeokkim + + 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. +*/ + +#include "ferox.h" +#include "stb_ds.h" + +/* | `world` 모듈 자료형 정의... | */ + +/* 물리 법칙이 존재하는 세계를 나타내는 구조체. */ +typedef struct frWorld { + Vector2 gravity; + frBody **bodies; + frSpatialHash *hash; + frCollision *collisions; + frCollisionHandler handler; + double accumulator; + double timestamp; + int *queries; +} frWorld; + +/* | `world` 모듈 함수... | */ + +/* 세계 `world`의 업데이트 이전 작업을 실행한다. */ +static void frPreUpdateWorld(frWorld *world); + +/* 세계 `world`의 업데이트 이후 작업을 실행한다. */ +static void frPostUpdateWorld(frWorld *world); + +/* 세계 `world`를 시간 `dt` (단위: ms)만큼 업데이트한다. */ +static void frUpdateWorld(frWorld *world, double dt); + +/* 중력 가속도가 `gravity`이고 경계 범위가 `bounds`인 세계를 생성한다. */ +frWorld *frCreateWorld(Vector2 gravity, Rectangle bounds) { + frWorld *result = calloc(1, sizeof(*result)); + + result->gravity = gravity; + result->hash = frCreateSpatialHash(bounds, FR_BROADPHASE_CELL_SIZE); + result->timestamp = frGetCurrentTime(); + + arrsetcap(result->bodies, FR_WORLD_MAX_BODY_COUNT); + arrsetcap(result->collisions, FR_WORLD_MAX_BODY_COUNT * FR_WORLD_MAX_BODY_COUNT); + arrsetcap(result->queries, FR_WORLD_MAX_BODY_COUNT); + + return result; +} + +/* 세계 `world`에 할당된 메모리를 해제한다. */ +void frReleaseWorld(frWorld *world) { + if (world == NULL) return; + + for (int i = 0; i < arrlen(world->bodies); i++) { + frBody *b = world->bodies[i]; + frShape *s = frGetBodyShape(b); + + frReleaseShape(s); + frReleaseBody(b); + } + + frReleaseSpatialHash(world->hash); + + arrfree(world->bodies); + arrfree(world->collisions); + arrfree(world->queries); + + free(world); +} + +/* 세계 `world`에 강체 `b`를 추가한다. */ +bool frAddToWorld(frWorld *world, frBody *b) { + if (world == NULL || b == NULL || arrlen(world->bodies) >= FR_WORLD_MAX_BODY_COUNT) + return false; + + arrput(world->bodies, b); + + return true; +} + +/* 세계 `world`의 모든 강체를 제거한다. */ +void frClearWorld(frWorld *world) { + if (world == NULL) return; + + frClearSpatialHash(world->hash); + + arrsetlen(world->bodies, 0); + arrsetlen(world->collisions, 0); +} + +/* 세계 `world`에서 강체 `b`를 제거한다. */ +bool frRemoveFromWorld(frWorld *world, frBody *b) { + if (world == NULL || b == NULL) return false; + + for (int i = 0; i < arrlen(world->bodies); i++) { + if (world->bodies[i] == b) { + arrdeln(world->bodies, i, 1); + + return true; + } + } + + return false; +} + +/* 구조체 `frWorld`의 크기를 반환한다. */ +size_t frGetWorldStructSize(void) { + return sizeof(frWorld); +} + +/* 세계 `world`에서 인덱스가 `index`인 강체의 메모리 주소를 반환한다. */ +frBody *frGetWorldBody(frWorld *world, int index) { + return (world != NULL && index >= 0 && index < arrlen(world->bodies)) + ? world->bodies[index] + : NULL; +} + +/* 세계 `world`의 충돌 핸들러를 반환한다. */ +frCollisionHandler frGetWorldCollisionHandler(frWorld *world) { + return (world != NULL) ? world->handler : FR_STRUCT_ZERO(frCollisionHandler); +} + +/* 세계 `world`의 강체 배열의 크기를 반환한다. */ +int frGetWorldBodyCount(frWorld *world) { + return (world != NULL) ? arrlen(world->bodies) : 0; +} + +/* 세계 `world`의 경계 범위를 반환한다. */ +Rectangle frGetWorldBounds(frWorld *world) { + return (world != NULL) + ? frGetSpatialHashBounds(world->hash) + : FR_STRUCT_ZERO(Rectangle); +} + +/* 세계 `world`의 공간 해시맵을 반환한다. */ +frSpatialHash *frGetWorldSpatialHash(frWorld *world){ + return (world != NULL) ? world->hash : NULL; +} + +/* 세계 `world`의 중력 가속도를 반환한다. */ +Vector2 frGetWorldGravity(frWorld *world) { + return (world != NULL) ? world->gravity : FR_STRUCT_ZERO(Vector2); +} + +/* 강체 `b`가 세계 `world`의 경계 범위 안에 있는지 확인한다. */ +bool frIsInWorldBounds(frWorld *world, frBody *b) { + if (world == NULL || b == NULL) return false; + + return CheckCollisionRecs(frGetBodyAABB(b), frGetWorldBounds(world)); +} + +/* 세계 `world`의 경계 범위를 `bounds`로 설정한다. */ +void frSetWorldBounds(frWorld *world, Rectangle bounds) { + if (world != NULL) frSetSpatialHashBounds(world->hash, bounds); +} + +/* 세계 `world`의 충돌 핸들러를 `handler`로 설정한다. */ +void frSetWorldCollisionHandler(frWorld *world, frCollisionHandler handler) { + if (world != NULL) world->handler = handler; +} + +/* 세계 `world`의 중력 가속도를 `gravity`로 설정한다. */ +void frSetWorldGravity(frWorld *world, Vector2 gravity) { + if (world != NULL) world->gravity = gravity; +} + +/* 세계 `world`의 시간을 `dt` (단위: ms)만큼 흐르게 한다. */ +void frSimulateWorld(frWorld *world, double dt) { + if (world == NULL || dt == 0.0f) return; + + double currentTime = frGetCurrentTime(); + double elapsedTime = frGetTimeDifference(currentTime, world->timestamp); + + world->accumulator += elapsedTime; + world->timestamp = currentTime; + + if (world->accumulator > FR_WORLD_ACCUMULATOR_LIMIT) + world->accumulator = FR_WORLD_ACCUMULATOR_LIMIT; + + for (; world->accumulator >= dt; world->accumulator -= dt) + frUpdateWorld(world, dt); +} + +/* 세계 `world`에서 직사각형 `rec`와 경계 범위가 겹치는 모든 강체를 반환한다. */ +int frQueryWorldSpatialHash(frWorld *world, Rectangle rec, frBody **bodies) { + if (world == NULL || bodies == NULL) return 0; + + arrsetlen(world->queries, 0); + + for (int i = 0; i < arrlen(world->bodies); i++) + frAddToSpatialHash(world->hash, frGetBodyAABB(world->bodies[i]), i); + + frQuerySpatialHash(world->hash, rec, &world->queries); + + int result = arrlen(world->queries); + + for (int i = 0; i < result; i++) + bodies[i] = world->bodies[world->queries[i]]; + + frClearSpatialHash(world->hash); + + return result; +} + +/* 세계 `world`의 모든 강체에 광선을 투사한다. */ +int frComputeWorldRaycast(frWorld *world, frRay ray, frRaycastHit *hits) { + if (world == NULL || hits == NULL) return 0; + + arrsetlen(world->queries, 0); + + for (int i = 0; i < arrlen(world->bodies); i++) + frAddToSpatialHash(world->hash, frGetBodyAABB(world->bodies[i]), i); + + Vector2 p1 = ray.origin, p2 = frVec2Add( + ray.origin, + frVec2ScalarMultiply( + frVec2Normalize(ray.direction), + ray.maxDistance + ) + ); + + Rectangle rec = (Rectangle) { + .x = fminf(p1.x, p2.x), + .y = fminf(p1.y, p2.y), + .width = fabsf(p2.x - p1.x), + .height = fabsf(p2.y - p1.y) + }; + + frQuerySpatialHash(world->hash, rec, &world->queries); + + if (arrlen(world->queries) <= 0) return 0; + + if (ray.closest) { + float minDistance = FLT_MAX; + + for (int i = 0; i < arrlen(world->queries); i++) { + frBody *body = world->bodies[world->queries[i]]; + + frRaycastHit raycast = frComputeBodyRaycast(body, ray); + + if (!raycast.check) continue; + + if (minDistance > raycast.distance) { + minDistance = raycast.distance; + hits[0] = raycast; + } + } + + frClearSpatialHash(world->hash); + + return 1; + } else { + int count = 0; + + for (int i = 0; i < arrlen(world->queries); i++) { + frBody *body = world->bodies[world->queries[i]]; + + frRaycastHit raycast = frComputeBodyRaycast(body, ray); + + if (!raycast.check) continue; + + hits[count++] = raycast; + } + + frClearSpatialHash(world->hash); + + return count; + } +} + +/* 세계 `world`의 업데이트 이전 작업을 실행한다. */ +static void frPreUpdateWorld(frWorld *world) { + if (world == NULL || world->hash == NULL) return; + + for (int i = 0; i < arrlen(world->bodies); i++) + frAddToSpatialHash(world->hash, frGetBodyAABB(world->bodies[i]), i); + + for (int i = 0; i < arrlen(world->bodies); i++) { + frQuerySpatialHash( + world->hash, + frGetBodyAABB(world->bodies[i]), + &world->queries + ); + + for (int k = 0; k < arrlen(world->queries); k++) { + int j = world->queries[k]; + + if (j <= i) continue; + + frBody *b1 = world->bodies[i]; + frBody *b2 = world->bodies[j]; + + frCollision collision = frComputeBodyCollision(b1, b2); + + if (collision.check) { + collision.cache.bodies[0] = b1; + collision.cache.bodies[1] = b2; + + arrput(world->collisions, collision); + } + } + + arrsetlen(world->queries, 0); + } +} + +/* 세계 `world`의 업데이트 이후 작업을 실행한다. */ +static void frPostUpdateWorld(frWorld *world) { + if (world == NULL || world->hash == NULL) return; + + for (int i = 0; i < arrlen(world->bodies); i++) + frClearBodyForces(world->bodies[i]); + + arrsetlen(world->collisions, 0); + + frClearSpatialHash(world->hash); +} + +/* 세계 `world`를 시간 `dt` (단위: ms)만큼 업데이트한다. */ +static void frUpdateWorld(frWorld *world, double dt) { + if (world == NULL || world->bodies == NULL || world->collisions == NULL) return; + + frPreUpdateWorld(world); + + // 강체 사이의 충돌 해결 직전에 사전 정의된 함수를 호출한다. + for (int i = 0; i < arrlen(world->collisions); i++) { + frCollisionCallback preSolveCallback = world->handler.preSolve; + + if (preSolveCallback != NULL) preSolveCallback(&world->collisions[i]); + } + + // 강체에 중력을 적용하고, 강체의 속도와 각속도를 계산한다. + for (int i = 0; i < arrlen(world->bodies); i++) { + frApplyGravity(world->bodies[i], world->gravity); + frIntegrateForBodyVelocities(world->bodies[i], dt); + } + + // 순차적으로 충격량을 반복 적용하여, 두 강체 사이의 충돌을 해결한다. + for (int i = 0; i < FR_WORLD_MAX_ITERATIONS; i++) + for (int j = 0; j < arrlen(world->collisions); j++) + frResolveCollision(&world->collisions[j]); + + // 강체의 현재 위치를 계산한다. + for (int i = 0; i < arrlen(world->bodies); i++) + frIntegrateForBodyPosition(world->bodies[i], dt); + + float inverseDt = (dt != 0.0f) ? (1.0f / dt) : 0.0f; + + // 강체의 위치를 적절하게 보정한다. + for (int i = 0; i < arrlen(world->collisions); i++) + frCorrectBodyPositions(&world->collisions[i], inverseDt); + + // 강체 사이의 충돌 해결 직후에 사전 정의된 함수를 호출한다. + for (int i = 0; i < arrlen(world->collisions); i++) { + frCollisionCallback postSolveCallback = world->handler.postSolve; + + if (postSolveCallback != NULL) postSolveCallback(&world->collisions[i]); + } + + frPostUpdateWorld(world); +}