initial commit

This commit is contained in:
moss 2025-09-17 21:34:45 -05:00
commit eb1e7a451e
11 changed files with 2291 additions and 0 deletions

11
README.md Normal file
View File

@ -0,0 +1,11 @@
lref is a c99 reference implementation of the physics of line rider beta 2 revision 6.2.
the goal is to byte-for-byte match beta 6.2's results on any given track, such that it can be a drop in replacement for unit tests of more modern physics engines.
it is also the goal to be readable enough to reference for future physics engines.
it is currently incomplete, but the raylib frontend in this repository can be used to test it against existing .sol track files.
the quality of that code and program is significantly lower than lref.h and only exists for testing purposes.
### references used to create lref
- [OpenLR](https://github.com/kevansevans/OpenLR)
- line rider beta 2 revision 6.2, decompiled

9
build.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
# this isnt meant to be guarenteed to work on whatever system
# the c compiler invokation just got really big
# works for my system, is a reference for you in the worst case
cd track_wrapper && zig build --release=fast && cd ..
clang -Wall -Wextra -pedantic -std=c99 -L/usr/local/lib -I/usr/local/include -I. raylib_frontend.c track_wrapper/zig-out/lib/libtrack_wrapper.a -lraylib -lm

644
lref.h Normal file
View File

@ -0,0 +1,644 @@
// 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

64
raylib_frontend.c Normal file
View File

@ -0,0 +1,64 @@
#define LREF_H_IMPLEMENTATION
#include "lref.h"
#include "track_wrapper/track_wrapper.h"
#include <raylib.h>
int main(int argc, char** argv) {
if (argc < 3) return EXIT_FAILURE;
uint32_t index = atoi(argv[1]);
struct simulation sim = simulation_new();
load_sol_track(&sim, argv[2], index);
bool playing = false;
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "lref test frontend");
Camera2D camera = { 0 };
camera.offset = (Vector2){screenWidth/2.0f, screenHeight/2.0f};
camera.rotation = 0.0f;
camera.zoom = 4.0f;
SetTargetFPS(40);
while (!WindowShouldClose()) {
Vector2 camera_target = {0};
for (int i = 0; i < POINT_COUNT; i++) {
camera_target.x += sim.bosh.points[i].x;
camera_target.y += sim.bosh.points[i].y;
}
camera_target.x /= POINT_COUNT;
camera_target.y /= POINT_COUNT;
camera.target = camera_target;
if (IsKeyPressed(KEY_SPACE)) {
playing = !playing;
}
BeginDrawing();
ClearBackground(WHITE);
BeginMode2D(camera);
{// draw bosh
for (int i = 0; i < BONE_COUNT; i++) {
struct bone bone = sim.bosh.bones[i];
struct point A = sim.bosh.points[bone.a];
struct point B = sim.bosh.points[bone.b];
DrawLineV((Vector2){A.x, A.y}, (Vector2){B.x, B.y}, BLACK);
}
}
{// draw lines
for (size_t i = 0; i < sim.line_count; i++) {
struct line line = sim.lines[i];
DrawLineV((Vector2){line.x1, line.y1}, (Vector2){line.x2, line.y2}, BLACK);
}
}
EndMode2D();
EndDrawing();
if (playing || IsKeyPressed(KEY_RIGHT)) simulation_step_frame(&sim);
}
CloseWindow();
return 0;
}

59
track_wrapper/build.zig Normal file
View File

@ -0,0 +1,59 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// This creates a "module", which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Every executable or library we compile will be based on one or more modules.
const lib_mod = b.createModule(.{
// `root_source_file` is the Zig "entry point" of the module. If a module
// only contains e.g. external object files, you can make this `null`.
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.pic = true,
});
// Now, we will create a static library based on the module we created above.
// This creates a `std.Build.Step.Compile`, which is the build step responsible
// for actually invoking the compiler.
const lib = b.addLibrary(.{
.linkage = .static,
.name = "track_wrapper",
.root_module = lib_mod,
});
// This declares intent for the library to be installed into the standard
// location when the user invokes the "install" step (the default step when
// running `zig build`).
b.installArtifact(lib);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const lib_unit_tests = b.addTest(.{
.root_module = lib_mod,
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
}

View File

@ -0,0 +1,47 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .track_wrapper,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0x16ca1ccf830cc3bc, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.14.1",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

184
track_wrapper/src/amf0.zig Normal file
View File

@ -0,0 +1,184 @@
const std = @import("std");
pub const Type = AmfType;
pub const AmfType = enum(u8) {
Number = 0,
Bool = 1,
String = 2,
Object = 3,
// MovieClip = 4,
Null = 5,
Undefined = 6,
// Reference = 7,
Array = 8,
ObjectEnd = 9,
// StrictArray = 10,
// Date = 11,
// LongString = 12,
// Unsupported = 13,
// RecordSet = 14,
// Xml = 15,
// TypedObject = 16,
pub fn zigTypeOfValue(t: AmfType) type {
return switch (t) {
.Number => f64,
.Bool => bool,
.String => []u8,
.Object => std.StringHashMap(AmfValue),
.Null, .Undefined, .ObjectEnd => void,
.Array => std.ArrayList(AmfValue),
};
}
};
pub const Number = AmfType.zigTypeOfValue(.Number);
pub const Bool = AmfType.zigTypeOfValue(.Bool);
pub const String = AmfType.zigTypeOfValue(.String);
pub const Object = AmfType.zigTypeOfValue(.Object);
pub const Array = AmfType.zigTypeOfValue(.Array);
pub const Value = AmfValue;
pub const AmfValue = struct {
amf_type: AmfType,
data: *anyopaque,
name: []u8,
alloc: std.mem.Allocator,
pub fn expectAs(self: AmfValue, comptime t: AmfType) !AmfType.zigTypeOfValue(t) {
if (self.amf_type != t) return error.TypeMismatch;
return @as(*AmfType.zigTypeOfValue(t), @ptrCast(@alignCast(self.data))).*;
}
pub fn deinit(self: *@This()) void {
switch (self.amf_type) {
.Object => {
var obj: *Object = @ptrCast(@alignCast(self.data));
var iter = obj.valueIterator();
while (iter.next()) |field_amf| {
deinit(field_amf);
}
},
.Array => {
const arr: *Array = @ptrCast(@alignCast(self.data));
for (arr.items) |*item_amf| {
deinit(item_amf);
}
},
.String => {
const contents: *[]u8 = @as(*[]u8, @ptrCast(@alignCast(self.data)));
self.alloc.free(contents.*);
},
.Bool => {
self.alloc.destroy(@as(*bool, @ptrCast(self.data)));
},
.Number => {
self.alloc.destroy(@as(*f64, @ptrCast(@alignCast(self.data))));
},
else => {},
}
self.alloc.free(self.name);
}
//pub fn readObjectFields(alloc: std.mem.Allocator, reader: anytype, object: *AmfValue) !void {
// if (object.amf_type != .Object) return error.PassedNonObject;
// const fields: *Object = @ptrCast(@alignCast(object.data));
// while (true) {
// const val = try read(alloc, reader, false);
// if (val.amf_type == .ObjectEnd) break;
// try fields.put(val.name, val);
// }
//}
//pub fn readArrayItems(alloc: std.mem.Allocator, reader: anytype, array: *AmfValue) !void {
// if (array.amf_type != .Array) return error.PassedNonArray;
// const items: *Array = @ptrCast(array.data);
// while (true) {
// const val = try read(alloc, reader, false);
// if (val.amf_type == .ObjectEnd) break;
// try items.append(alloc, val);
// }
//}
pub fn read(alloc: std.mem.Allocator, reader: anytype) !AmfValue {
const name: []u8 = try alloc.alloc(u8, std.math.cast(usize, try reader.readInt(i16, .big)) orelse return error.CastError);
_ = try reader.readAll(name);
const amf_t = std.meta.intToEnum(AmfType, reader.readInt(u8, .big) catch return error.DoneReading) catch return error.UnimplementedAmfType;
var result = AmfValue{ .name = name, .amf_type = amf_t, .alloc = alloc, .data = undefined };
switch (amf_t) {
.Number => {
const ptr = try alloc.create(Number);
ptr.* = @bitCast(try reader.readInt(u64, .big));
result.data = @ptrCast(ptr);
},
.Bool => {
const ptr = try alloc.create(Bool);
ptr.* = try reader.readByte() != 0;
result.data = @ptrCast(ptr);
},
.String => {
const len = try reader.readInt(i16, .big);
const ptr = try alloc.create(String);
ptr.* = try alloc.alloc(u8, std.math.cast(usize, len) orelse return error.CastError);
_ = try reader.readAll(ptr.*);
result.data = @ptrCast(ptr);
},
.Object => {
const ptr = try alloc.create(Object);
ptr.* = Object.init(alloc);
result.data = @ptrCast(ptr);
//try readObjectFields(alloc, reader, &result);
const fields: *Object = ptr;
while (true) {
const val = try read(alloc, reader);
if (val.amf_type == .ObjectEnd) break;
try fields.put(val.name, val);
}
},
.Null, .Undefined, .ObjectEnd => {},
.Array => {
const ptr = try alloc.create(Array);
const len = try reader.readInt(i32, .big);
ptr.* = try Array.initCapacity(alloc, std.math.cast(usize, len) orelse return error.CastError);
result.data = @ptrCast(ptr);
//try readArrayItems(alloc, reader, result);
const items: *Array = ptr;
while (true) {
const val = try read(alloc, reader);
if (val.amf_type == .ObjectEnd) break;
try items.append(val);
}
},
}
return result;
}
pub fn getProperty(self: AmfValue, name: []const u8) !AmfValue {
if (self.amf_type != .Object) return error.PassedNonObject;
const fields: *Object = @ptrCast(@alignCast(self.data));
const prop: AmfValue = fields.get(name) orelse return error.NonexistantProperty;
return prop;
}
pub fn getPropertyExpectType(self: AmfValue, name: []const u8, comptime expected_type: AmfType) !AmfType.zigTypeOfValue(expected_type) {
const prop = try self.getProperty(name);
if (prop.amf_type != expected_type) return error.TypeMismatch;
const ptr: *AmfType.zigTypeOfValue(expected_type) = @ptrCast(@alignCast(prop.data));
return ptr.*;
}
pub fn getItem(self: AmfValue, index: usize) !AmfValue {
if (self.amf_type != .Array) return error.PassedNonArray;
const arr: *Array = @ptrCast(@alignCast(self.data));
if (index >= arr.items.len) return error.OutOfBounds;
return arr.items[index];
}
pub fn getItemExpectType(self: AmfValue, index: usize, comptime expected_type: AmfType) !AmfType.zigTypeOfValue(expected_type) {
const item = try self.getItem(index);
if (item.amf_type != expected_type) return error.TypeMismatch;
const ptr: *AmfType.zigTypeOfValue(expected_type) = @ptrCast(@alignCast(item.data));
return ptr.*;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
const std = @import("std");
const c = @import("include.zig");
const sol = @import("sol.zig");
const amf = @import("amf0.zig");
pub export fn load_sol_track(sim: [*c]c.struct_simulation, filename: [*c]u8, index: u32) void {
const file = std.fs.cwd().openFile(std.mem.sliceTo(filename, 0), .{}) catch return;
defer file.close();
var sol_f = sol.readSol(std.heap.smp_allocator, file.reader()) catch return;
defer sol_f.deinit();
const tracks = sol.getTracks(sol_f) catch return;
const track = tracks.items[index];
const startprop = track.getProperty("startLine") catch return;
const lines: amf.Array = track.getPropertyExpectType("data", .Array) catch return;
if (startprop.amf_type == .Number) {
const ID: u32 = std.math.cast(u32, @as(u32, @intFromFloat(@as(*f64, @ptrCast(@alignCast(startprop.data))).*))) orelse return;
var maybe_line: ?amf.AmfValue = null;
for (lines.items) |iter_line| {
const iter_ID: u32 = std.math.cast(u32, @as(u32, @intFromFloat(iter_line.getItemExpectType(8, .Number) catch return))) orelse return;
if (iter_ID == ID) {
maybe_line = iter_line;
break;
}
}
if (maybe_line) |amf_line| {
const line = sol.Line.fromAmf(amf_line) catch return;
sim.*.bosh = c.rider_new(line.x1, line.y1 - 25.0);
} else {
sim.*.bosh = c.rider_new(100.0, 100.0);
}
} else {
if (startprop.amf_type != .Array) return;
const x = startprop.getItemExpectType(0, .Number) catch return;
const y = startprop.getItemExpectType(1, .Number) catch return;
sim.*.bosh = c.rider_new(x, y);
}
c.simulation_prepare_line_capacity(sim, lines.items.len);
for (lines.items) |amf_line| {
const line = sol.Line.fromAmf(amf_line) catch continue;
if (line.linetype == .Scenery) continue;
const simline = c.struct_line{
.id = std.math.cast(i32, line.ID) orelse continue,
.x1 = line.x1,
.y1 = line.y1,
.x2 = line.x2,
.y2 = line.y2,
.flags = .{
.red = line.linetype == .Acceleration,
.invert = line.invert,
.left_extended = line.left_extended,
.right_extended = line.right_extended,
},
};
c.simulation_add_line(sim, simline);
}
// no fucking way !!!!!! its hypothetically done
}

73
track_wrapper/src/sol.zig Normal file
View File

@ -0,0 +1,73 @@
const std = @import("std");
const amf = @import("amf0.zig");
pub const Track = amf.Value;
pub fn readSol(alloc: std.mem.Allocator, reader: anytype) !amf.AmfValue {
const sol_version = try reader.readInt(i16, .big);
const length = try reader.readInt(i32, .big);
_ = sol_version;
_ = length;
if (try reader.readInt(i32, .big) != 0x5443534F) return error.InvalidMagicNumber;
try reader.skipBytes(6, .{});
const name: []u8 = try alloc.alloc(u8, std.math.cast(usize, try reader.readInt(i16, .big)) orelse return error.CastError);
_ = try reader.readAll(name);
if (!std.mem.eql(u8, name, "savedLines")) return error.InvalidSolName;
if (try reader.readInt(i32, .big) != 0) return error.InvalidAmfVersion;
const ptr = try alloc.create(amf.Object);
ptr.* = amf.Object.init(alloc);
const amf_obj = amf.AmfValue{ .name = name, .alloc = alloc, .amf_type = .Object, .data = @ptrCast(ptr) };
const fields: *amf.Object = ptr;
while (true) {
const val = amf.AmfValue.read(alloc, reader) catch |err| {
if (err == error.EndOfStream) break;
return err;
};
if (val.amf_type == .ObjectEnd) break;
try fields.put(val.name, val);
}
return amf_obj;
}
pub fn getTracks(root: amf.AmfValue) !amf.Array {
return root.getPropertyExpectType("trackList", .Array);
}
pub const LineType = enum(u2) {
Standard = 0,
Acceleration = 1,
Scenery = 2,
};
pub const Line = struct {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
invert: bool,
ID: u32,
linetype: LineType,
left_extended: bool,
right_extended: bool,
pub const Extension = packed struct(u2) {
left: bool,
right: bool,
};
pub fn fromAmf(line: amf.Value) !Line {
//std.debug.print("line: {any} 5: {any} 0: {any}", .{ line.amf_type, (try line.getItem(4)).amf_type, (try line.getItem(0)).amf_type });
const extensions: Extension = @bitCast(@as(u2, @intFromFloat(line.getItemExpectType(4, .Number) catch 0)));
return Line{
.x1 = try line.getItemExpectType(0, .Number),
.y1 = try line.getItemExpectType(1, .Number),
.x2 = try line.getItemExpectType(2, .Number),
.y2 = try line.getItemExpectType(3, .Number),
.invert = (line.getItemExpectType(5, .Number) catch 0) != 0,
.left_extended = extensions.left,
.right_extended = extensions.right,
.ID = @as(u32, @intFromFloat(try line.getItemExpectType(8, .Number))),
.linetype = @enumFromInt(@as(u2, @intFromFloat(try line.getItemExpectType(9, .Number)))),
};
}
};

View File

@ -0,0 +1,10 @@
// i already wrote sol parsing in zig so
// this is just some stupid glue code to
// use that instead of dealing with it
// all over again in c
// this is scuffed but um i uncomment this is i need to generate include.zig
// but leave it commented for when ralib_crontend.c includes it
// #include "lref.h"
void load_sol_track(struct simulation* sim, char* filename, uint32_t index);