lref/lref.h
2025-09-17 21:34:45 -05:00

645 lines
18 KiB
C

// LREF:
//
// c99 reference implementation of line rider,
// with the goal of exactly matching beta 6.2.
//
// ~moss7
// code todos
// TODO 6.1 and 6.0 grid registration
// TODO camera implementation
// TODO cleanup / deallocation logic
// TODO consider which functions actually need to be api vs internal
// documentation todos
// TODO equivalent flash decomp code in comments for reference
// TODO explain all of the physics functions in comments
// TODO explain parts that may differ in a less contrived engine
// (NaN checks, optimizations not made here for readability, etc)
// # external includes
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
// these only need to be included by the implementation
#ifdef LREF_H_IMPLEMENTATION
#include <math.h>
#include <stdlib.h>
#include <string.h>
#endif
// # rider constants
const double endurance = 0.057;
const double gravity = 0.175;
#define POINT_COUNT 10
#define BONE_COUNT 22
#define ITERATION_COUNT 6
// # rider types
struct point {
double x;
double y;
double previous_x;
double previous_y;
double momentum_x;
double momentum_y;
double friction;
};
struct bone {
bool breakable;
bool repel;
uint8_t a;
uint8_t b;
double restLength;
};
struct rider {
struct point points[POINT_COUNT];
struct bone bones[BONE_COUNT];
bool dead;
bool sledbroken;
};
// # line constants
const double ZONE = 10;
const double ACC = 0.1;
// # line types
struct line_flags {
bool red;
bool invert;
bool left_extended;
bool right_extended;
};
struct line_values {
double dx;
double dy;
double invSqrDst;
double nx;
double ny;
double lim1;
double lim2;
double accx;
double accy;
};
struct line {
int32_t id;
double x1;
double y1;
double x2;
double y2;
struct line_flags flags;
struct line_values values;
};
// # grid constants
const uint8_t GRID_62 = 0;
const uint8_t GRID_61 = 1;
const uint8_t GRID_60 = 2;
const double GRIDSIZE = 14;
#define GRID_ARRAY_BASE_SIZE 65536
// # grid types
struct grid_coordinate {
int64_t x;
int64_t y;
};
struct grid_bucket {
struct grid_coordinate key;
struct grid_cell_entry* entry;
struct grid_bucket* next;
};
struct grid_cell_entry {
struct line* line;
struct grid_cell_entry* next;
};
struct grid {
struct grid_bucket** buckets;
size_t capacity;
size_t cells_total;
};
// integer grid coordinate plus the fractional remainder from truncation
struct grid_pos {
struct grid_coordinate coord;
double rx;
double ry;
};
// # simulation types
struct simulation {
double start_x;
double start_y;
struct grid* grid;
struct line* lines;
size_t line_count;
struct rider bosh;
};
// # line function declarations
void line_calculate_values(struct line* line);
void line_collide(const struct line line, struct point* point);
// # grid function declarations
struct grid* grid_new(void);
void grid_free(struct grid* grid);
void grid_register_62(struct grid* grid, struct line* line);
void grid_register_61(struct grid* grid, struct line* line);
void grid_register_60(struct grid* grid, struct line* line);
struct grid_coordinate grid_coordinate_at(double x, double y);
struct grid_pos grid_pos_at(double x, double y);
// # rider function declarations
struct rider rider_new(double x, double y);
// # simulation function declarations
struct simulation simulation_new(void);
void simulation_prepare_line_capacity(struct simulation* simulation, size_t capacity);
void simulation_add_line(struct simulation* simulation, struct line line);
#ifdef LREF_H_IMPLEMENTATION
// # line function implementations
void line_calculate_values(struct line* line) {
line->values.dx = line->x2 - line->x1;
line->values.dy = line->y2 - line->y1;
double sqrDst = (line->values.dx * line->values.dx + line->values.dy * line->values.dy);
line->values.invSqrDst = 1.0 / sqrDst;
double dst = sqrt(sqrDst);
double invDst = 1.0 / dst;
line->values.nx = line->values.dy * invDst * (line->flags.invert ? 1 : -1);
line->values.ny = line->values.dx * invDst * (line->flags.invert ? -1 : 1);
double lim = fmin(0.25, ZONE / dst);
line->values.lim1 = line->flags.left_extended ? -lim : 0;
line->values.lim2 = line->flags.right_extended ? 1 + lim : 1;
if (!line->flags.red) return;
line->values.accx = line->values.ny * ACC * (line->flags.invert ? 1 : -1);
line->values.accy = line->values.nx * ACC * (line->flags.invert ? -1 : 1);
}
void line_collide(const struct line line, struct point* point) {
if (point->momentum_x * line.values.nx + point->momentum_y * line.values.ny <= 0.0) return;
double distx = point->x - line.x1;
double disty = point->y - line.y1;
double distance_perpendicular = line.values.nx * distx + line.values.ny * disty;
double distance_along = (distx * line.values.dx + disty * line.values.dy) * line.values.invSqrDst;
if (distance_perpendicular > 0 && distance_perpendicular < ZONE &&
distance_along >= line.values.lim1 && distance_along <= line.values.lim2) {
point->x -= distance_perpendicular * line.values.nx;
point->y -= distance_perpendicular * line.values.ny;
point->previous_x += line.values.ny * point->friction * distance_perpendicular
* (point->previous_x < point->x ? 1 : -1) + (line.flags.red ? line.values.accx : 0);
point->previous_y -= line.values.nx * point->friction * distance_perpendicular
* (point->previous_y < point->y ? -1 : 1) + (line.flags.red ? line.values.accy : 0);
}
}
// # grid hashtable function implementations
struct grid* grid_new(void) {
struct grid* grid = calloc(1, sizeof(struct grid));
grid->buckets = calloc(GRID_ARRAY_BASE_SIZE, sizeof(struct grid_bucket*));
grid->capacity = GRID_ARRAY_BASE_SIZE;
grid->cells_total = 0;
return grid;
}
uint64_t grid_hash(const struct grid_coordinate coords) {
uint64_t x = coords.x;
uint64_t y = coords.y;
x ^= y + 0x9e3779b9 + (x << 6) + (x >> 2);
return x;
}
size_t grid_index(const struct grid grid, const struct grid_coordinate coords) {
return (size_t)grid_hash(coords) & (grid.capacity - 1);
}
bool grid_keys_eql(const struct grid_coordinate a, const struct grid_coordinate b) {
return a.x == b.x && a.y == b.y;
}
bool grid_contains_cell(const struct grid grid, const struct grid_coordinate coords) {
size_t index = grid_index(grid, coords);
struct grid_bucket* current = grid.buckets[index];
while (current != NULL) {
if (grid_keys_eql(current->key, coords)) return true;
current = current->next;
}
return false;
}
void grid_put_ptr(struct grid* grid, struct grid_bucket* cell) {
size_t index = grid_index(*grid, cell->key);
struct grid_bucket* current = grid->buckets[index];
if (current == NULL) {
grid->buckets[index] = cell;
cell->next = NULL;
return;
}
while (current->next != NULL) {
// if we are overwriting the old pointer
// (i dont think this ever actually happens
// in lref but for correctness, whatever)
if (grid_keys_eql(current->next->key, cell->key)) {
cell->next = current->next->next;
current->next = cell;
return;
}
current = current->next;
}
current->next = cell;
cell->next = NULL;
grid->cells_total += 1;
}
bool grid_should_double_capacity(const struct grid grid) {
return (grid.cells_total * 4 > grid.capacity * 3);
}
void grid_double_capacity(struct grid* grid) {
size_t old_capacity = grid->capacity;
struct grid_bucket** old_buckets = grid->buckets;
grid->capacity *= 2;
grid->buckets = calloc(grid->capacity, sizeof(struct grid_bucket*));
for (unsigned int i = 0; i < old_capacity; i++) {
struct grid_bucket* next_bucket = old_buckets[i];
while (next_bucket != NULL) {
struct grid_bucket* current_bucket = next_bucket;
next_bucket = current_bucket->next;
grid_put_ptr(grid, current_bucket);
}
}
free(old_buckets);
}
struct grid_bucket* grid_cell_new(struct grid_coordinate coords) {
struct grid_bucket* cell = calloc(1, sizeof(struct grid_bucket));
cell->key = coords;
cell->next = NULL;
cell->entry = NULL;
return cell;
}
// get the cell at a given coordinate, creating it if it doesnt exist
struct grid_bucket* grid_get_cell(struct grid* grid, const struct grid_coordinate coords) {
size_t index = grid_index(*grid, coords);
struct grid_bucket* current = grid->buckets[index];
if (current == NULL) {
grid->buckets[index] = grid_cell_new(coords);
return grid->buckets[index];
}
if (grid_keys_eql(current->key, coords)) return current;
while (current->next != NULL) {
if (grid_keys_eql(current->next->key, coords)) return current->next;
current = current->next;
}
current->next = grid_cell_new(coords);
return current->next;
}
struct grid_coordinate grid_coordinate_at(double x, double y) {
struct grid_coordinate coord = { .x = (int64_t)floor(x / GRIDSIZE), .y = (int64_t)floor(y / GRIDSIZE) };
return coord;
}
struct grid_pos grid_pos_at(double x, double y) {
struct grid_coordinate coord = grid_coordinate_at(x, y);
struct grid_pos pos = {
.coord = coord,
.rx = x - GRIDSIZE * coord.x,
.ry = y - GRIDSIZE * coord.y,
};
return pos;
}
// # grid line registry function implementations
void grid_cell_add_line(struct grid_bucket* cell, struct line* line) {
if (cell->entry == NULL) {
struct grid_cell_entry* entry = calloc(1, sizeof(struct grid_cell_entry));
entry->line = line;
cell->entry = entry;
return;
}
struct grid_cell_entry* current = cell->entry;
if (current->line->id < line->id) {
struct grid_cell_entry* entry = calloc(1, sizeof(struct grid_cell_entry));
entry->line = line;
entry->next = current;
cell->entry = entry;
return;
}
while (current->next != NULL) {
if (current->next->line->id < line->id) {
struct grid_cell_entry* entry = calloc(1, sizeof(struct grid_cell_entry));
entry->line = line;
entry->next = current->next;
current->next = entry;
return;
}
current = current->next;
}
struct grid_cell_entry* entry = calloc(1, sizeof(struct grid_cell_entry));
entry->line = line;
current->next = entry;
}
void grid_register_62(struct grid* grid, struct line* line) {
struct grid_pos start = grid_pos_at(line->x1, line->y1);
struct grid_pos end = grid_pos_at(line->x2, line->y2);
double right = line->values.dx > 0 ? end.coord.x : start.coord.x;
double left = line->values.dx > 0 ? start.coord.x : end.coord.x;
double bottom = line->values.dy > 0 ? end.coord.y : start.coord.y;
double top = line->values.dy > 0 ? start.coord.y : end.coord.y;
grid_cell_add_line(grid_get_cell(grid, start.coord), line);
if ((line->values.dx == 0 && line->values.dy == 0) ||
(start.coord.x == end.coord.x && start.coord.y == end.coord.y)) return;
double x = line->x1;
double y = line->y1;
double invDx = 1.0 / line->values.dx;
double invDy = 1.0 / line->values.dy;
double difX;
double difY;
while (true) {
if (start.coord.x < 0) {
difX = line->values.dx > 0 ? (GRIDSIZE + start.rx) : (-GRIDSIZE - start.rx);
} else {
difX = line->values.dx > 0 ? (GRIDSIZE - start.rx) : (-(start.rx + 1));
}
if (start.coord.y < 0) {
difY = line->values.dy > 0 ? (GRIDSIZE + start.ry) : (-GRIDSIZE - start.ry);
} else {
difY = line->values.dy > 0 ? (GRIDSIZE - start.ry) : (-(start.ry + 1));
}
if (line->values.dx == 0) {
y += difY;
} else if (line->values.dy == 0) {
x += difX;
} else {
double step = y + line->values.dy * difX * invDx;
if (fabs(step - y) < fabs(difY)) {
x += difX;
y = step;
} else if (fabs(step - y) == fabs(difY)) {
x += difX;
y += difY;
} else {
x += line->values.dx * difY * invDy;
y += difY;
}
}
start = grid_pos_at(x, y);
if (start.coord.x >= left && start.coord.x <= right && start.coord.y >= top && start.coord.y <= bottom) {
grid_cell_add_line(grid_get_cell(grid, start.coord), line);
continue;
}
return;
}
}
// # point function implementations
void point_apply_momentum(struct point* point) {
point->momentum_x = point->x - point->previous_x;
point->momentum_y = point->y - point->previous_y + gravity;
point->previous_x = point->x;
point->previous_y = point->y;
point->x += point->momentum_x;
point->y += point->momentum_y;
}
// # rider function implementations
void rider_init_point(struct rider* bosh, uint8_t index, double x, double y, double friction) {
bosh->points[index].x = x;
bosh->points[index].y = y;
bosh->points[index].previous_x = x - 0.4;
bosh->points[index].previous_y = y;
bosh->points[index].friction = friction;
}
void rider_init_bone(struct rider* bosh, uint8_t index, uint8_t a, uint8_t b, bool breakable, bool repel) {
bosh->bones[index].a = a;
bosh->bones[index].b = b;
bosh->bones[index].breakable = breakable;
bosh->bones[index].repel = repel;
double x = bosh->points[a].x - bosh->points[b].x;
double y = bosh->points[a].y - bosh->points[b].y;
bosh->bones[index].restLength = sqrt(x * x + y * y) * (repel ? 0.5 : 1.0);
}
void rider_apply_start_offset(struct rider* bosh, double x, double y) {
for (int i = 0; i < POINT_COUNT; i++) {
struct point* point = &bosh->points[i];
point->x += x;
point->y += y;
point->previous_x = point->x - 0.4;
point->previous_y = point->y;
}
}
struct rider rider_new(double x, double y) {
struct rider bosh;
memset(&bosh, 0, sizeof(bosh));
rider_init_point(&bosh, 0, 0.0, 0.0, 0.8);
rider_init_point(&bosh, 1, 0.0, 5.0, 0.0);
rider_init_point(&bosh, 2, 15.0, 5.0, 0.0);
rider_init_point(&bosh, 3, 17.5, 0.0, 0.0);
rider_init_point(&bosh, 4, 5.0, 0.0, 0.8);
rider_init_point(&bosh, 5, 5.0, -5.5, 0.8);
rider_init_point(&bosh, 6, 11.5, -5.0, 0.1);
rider_init_point(&bosh, 7, 11.5, -5.0, 0.1);
rider_init_point(&bosh, 8, 10.0, 5.0, 0.0);
rider_init_point(&bosh, 9, 10.0, 5.0, 0.0);
rider_init_bone(&bosh, 0, 0, 1, false, false);
rider_init_bone(&bosh, 1, 1, 2, false, false);
rider_init_bone(&bosh, 2, 2, 3, false, false);
rider_init_bone(&bosh, 3, 3, 0, false, false);
rider_init_bone(&bosh, 4, 0, 2, false, false);
rider_init_bone(&bosh, 5, 3, 1, false, false);
rider_init_bone(&bosh, 6, 0, 4, true, false);
rider_init_bone(&bosh, 7, 1, 4, true, false);
rider_init_bone(&bosh, 8, 2, 4, true, false);
rider_init_bone(&bosh, 9, 5, 4, false, false);
rider_init_bone(&bosh, 10, 5, 6, false, false);
rider_init_bone(&bosh, 11, 5, 7, false, false);
rider_init_bone(&bosh, 12, 4, 8, false, false);
rider_init_bone(&bosh, 13, 4, 9, false, false);
rider_init_bone(&bosh, 14, 5, 7, false, false);
rider_init_bone(&bosh, 15, 5, 0, true, false);
rider_init_bone(&bosh, 16, 3, 6, true, false);
rider_init_bone(&bosh, 17, 3, 7, true, false);
rider_init_bone(&bosh, 18, 8, 2, true, false);
rider_init_bone(&bosh, 19, 9, 2, true, false);
rider_init_bone(&bosh, 20, 5, 8, false, true);
rider_init_bone(&bosh, 21, 5, 9, false, true);
rider_apply_start_offset(&bosh, x, y);
return bosh;
}
void rider_bone_satisfy_single(struct rider* bosh, uint8_t bone_idx) {
struct bone bone = bosh->bones[bone_idx];
struct point* a = &bosh->points[bone.a];
struct point* b = &bosh->points[bone.b];
double x = a->x - b->x;
double y = a->y - b->y;
double dist = sqrt(x * x + y * y);
if (bone.repel && dist >= bone.restLength) return;
double effect = (dist - bone.restLength) / dist * 0.5;
if (bone.breakable) {
if (effect > endurance * bone.restLength * 0.5) {
bosh->dead = true;
return;
}
if (bosh->dead) return;
}
double xEffect = x * effect;
double yEffect = y * effect;
a->x -= xEffect;
a->y -= yEffect;
b->x += xEffect;
b->y += yEffect;
}
void rider_bone_satisfy_all(struct rider* bosh) {
for (uint8_t i = 0; i < BONE_COUNT; i++) {
rider_bone_satisfy_single(bosh, i);
}
}
void rider_apply_momentum(struct rider* bosh) {
for (uint8_t i = 0; i < POINT_COUNT; i++) {
point_apply_momentum(&bosh->points[i]);
}
}
void rider_fakie_check(struct rider* bosh) {
double along_sled_x = bosh->points[3].x - bosh->points[0].x;
double along_sled_y = bosh->points[3].y - bosh->points[0].y;
if (along_sled_x * (bosh->points[1].y - bosh->points[0].y) - along_sled_y * (bosh->points[1].x - bosh->points[0].x) < 0) {
bosh->dead = true;
bosh->sledbroken = true;
}
if (along_sled_x * (bosh->points[5].y - bosh->points[4].y) - along_sled_y * (bosh->points[5].x - bosh->points[4].x) > 0) {
bosh->dead = true;
}
}
// # simulation function implementations
struct simulation simulation_new(void) {
struct simulation sim = {
.grid = grid_new(),
.bosh = rider_new(0, 0),
};
return sim;
}
void simulation_collide_single_point_and_grid_cell(struct point* point, struct grid_bucket* cell) {
struct grid_cell_entry* current = cell->entry;
while (current != NULL) {
line_collide(*current->line, point);
current = current->next;
}
}
void simulation_collide_all(struct simulation* simulation) {
for (uint8_t i = 0; i < POINT_COUNT; i++) {
struct point* point = &simulation->bosh.points[i];
struct grid_coordinate base_coords = grid_pos_at(point->x, point->y).coord;
for (int64_t x = -1; x < 2; x++) {
for (int64_t y = -1; y < 2; y++) {
struct grid_coordinate cell_coords = base_coords;
cell_coords.x += x;
cell_coords.y += y;
struct grid_bucket* cell = grid_get_cell(simulation->grid, cell_coords);
simulation_collide_single_point_and_grid_cell(point, cell);
}
}
}
}
void simulation_step_frame(struct simulation* simulation) {
rider_apply_momentum(&simulation->bosh);
for (uint8_t i = 0; i < ITERATION_COUNT; i++) {
rider_bone_satisfy_all(&simulation->bosh);
simulation_collide_all(simulation);
}
rider_fakie_check(&simulation->bosh);
}
void simulation_prepare_line_capacity(struct simulation* simulation, size_t capacity) {
simulation->lines = realloc(simulation->lines, sizeof(struct line) * capacity);
}
void simulation_add_line(struct simulation* simulation, struct line line) {
line_calculate_values(&line);
simulation->lines[simulation->line_count] = line;
// TODO 61 & 60
grid_register_62(simulation->grid, &simulation->lines[simulation->line_count]);
simulation->line_count++;
}
#endif