Curves and web servers

(3146 words)

Last week, we started to think about curves. This week, we’ve mostly finished the curves in roads. There is still one thing to be done, which is reducing the radius of the curve to make it fit the space available, but this should not be too difficult, so can wait a bit.

However, to complete the curves, we had to add a clockwise argument to the draw_arc function.

const direction: f32 = if (clockwise) -1 else 1;

var angle: f32 = start;
const sweep: f32 =
    if (clockwise)
        @mod(start - end, std.math.tau)
    else
        @mod(end - start, std.math.tau);

const segments: usize =
    @max(3, @as(usize, @intFromFloat(num * sweep / std.math.tau)));

The important thing here is that the number of segments to draw should be related to the angular sweep from the start angle to the end angle. Once that is calculated, the way we draw the arc is pretty much the same as before.

The CurveUpdateRequired component feeds into the update_curves_system that does all the fun stuff.

fn update_curves_system(iter: *const ng.SystemIterator) void {
    for (iter.entities) |entity| {

First we iterate over all the entities that match this system.

const node = entity.get(com.Node) orelse continue;

const curve = entity.getPtr(com.Curve) orelse continue;

Then we get the node and a mutable curve components from this entity, otherwise we continue to the next entity.

const radius: ng.Vec2 = @splat(curve.radius);

const from_link = curve.from.get(com.Link) orelse return;
const from = if (from_link.start == entity) from_link.end else from_link.start;

const to_link = curve.to.get(com.Link) orelse return;
const to = if (to_link.start == entity) to_link.end else to_link.start;
const from_node = from.get(com.Node) orelse return;
const to_node = to.get(com.Node) orelse return;

We get a bunch of information from the curve. The from and to links are obtained. Note, that the curve could be at the start or end of each link, so we check the start field of the link and use that or the end field.

Finally, we get the Node component for the from and to entities.

At this point, we have all the data we need and we can get to the fun math bit.

const dfrom = ng.normalize(from_node.pos - node.pos);
const dto = ng.normalize(node.pos - to_node.pos);

const a = ng.atan2(dfrom);
const b = ng.atan2(dto);
const c = @mod(a - b, std.math.tau);

dfrom is the normalized vector that points from the from_node to the current node. dto is similar for the current node to the to_node. With these, we can calculate their angles using atan2 on these vectors. And with these angles, we can determine how much these angles ‘curve’.

const side: ng.Vec2 = if (c > std.math.pi) .{ 1, 1 } else .{ -1, -1 };

If the angle curves more than ‘pi’ then we use one or the other side.

const from_a = node.pos + ng.Vec2{ dfrom[1], -dfrom[0] } * radius * side;
const from_b = from_node.pos + ng.Vec2{ dfrom[1], -dfrom[0] } * radius * side;

const to_a = node.pos + ng.Vec2{ dto[1], -dto[0] } * radius * side;
const to_b = to_node.pos + ng.Vec2{ dto[1], -dto[0] } * radius * side;

We then compute two positions per side, from_a and from_b are based on the current node position, the dfrom vector, the radius and the side; to_a and to_b are computed the same way. This gives us two points per line on the correct side of the line, based on the radius of the curve. This allows us to compute the line intersection of these two lines to determine the position of the center of the curve.

curve.center = ng.line_intersection(from_a, from_b, to_a, to_b);

From here, it is really simple. We need to determine where the straight line of the link will be. We can get this point based on the center point of the curve, the tangent of the incoming line, and the side that this is on.

const from_tangent = ng.Vec2{ -dfrom[1], dfrom[0] };
const to_tangent = ng.Vec2{ -dto[1], dto[0] };

const end_link = curve.center + from_tangent * radius * side;

With all this information, we can determine the final parameters for the curve.

curve.start_angle = ng.atan2(from_tangent * side);
curve.end_angle = ng.atan2(to_tangent * side);
curve.clockwise = side[0] < 0;
curve.width = from_link.width;
curve.style = from_link.style;
curve.offset = ng.distance(end_link - node.pos);

entity.remove(com.CurveUpdateRequired);

The curve offset is just the distance from the end_link to the node.pos. And of course, given that we’ve updated the Curve of the entity, we can remove the CurveUpdateRequired component from this entity.

This week, I’ve added a number of new components:

LaneKind is an enumerated type that describes the different possible types of a lane.

LinkStyle is effectively an array of LaneKind and widths, that determines how different lanes of a link should be drawn and therefore what vehicles are allowed on different lanes. This is used when drawing lanes at the moment.

Junction is an array of links that define the arms that go into a given junction. Remember that Curve is used for nodes that have two links going to it, and Junction is used for nodes that have more than two links.

OnLink is a component that should be able to be placed on a vehicle entity when it is on a link. It encodes the link and the lane that this vehicle is within. This isn’t used at the moment, and I don’t think it is complete yet.

An example of a LinkStyle is used in the code today, using the add_lane function.

const s1 = ng.new();
add_lane(s1, .sidewalk, 2.0);
add_lane(s1, .kerb, 0.1);
add_lane(s1, .traffic_down, 3.2);
add_lane(s1, .center_line, 0.1);
add_lane(s1, .traffic_up, 3.2);
add_lane(s1, .kerb, 0.1);
add_lane(s1, .sidewalk, 2.0);

Here we have a link that has a sidewalk, a kerb, a traffic lane, a center line, another traffic lane, another kerb, and another sidewalk. All very self-explanatory. You can probably see how an editor could be easily built that allows the configuration of such styles to be built.

The add_lane function itself is pretty simple:

fn add_lane(entity: ng.Entity, kind: com.LaneKind, width: f32) void {
    const int_width = to_width(width);
    if (entity.getPtr(com.LinkStyle)) |style| {
        if (style.num_lanes < com.max_lanes) {
            style.kind[style.num_lanes] = kind;
            style.width[style.num_lanes] = int_width;
            style.total_width += int_width;
            style.num_lanes += 1;
        }
    } else {
        var style: com.LinkStyle = undefined;
        style.num_lanes = 1;
        style.kind[0] = kind;
        style.width[0] = int_width;
        style.total_width = int_width;

        entity.set(style);
    }
}

There are two cases to deal with. The first when the entity already has a LinkStyle, where we just add a lane based on the num_lanes field of the component. The second is where the entity does not have a LinkStyle, where we just set a LinkStyle on the entity with a single lane with the correct lane information.

One important aspect of this is that the style doesn’t enforce the width of the link. The link itself still has a width. This is because in many locations, the width of the right of way is typically wider than the lanes. This allows roads to be widened at a later date without requiring any additional land. For example, major roads are typically built within a right of way that is much wider than originally built road. Later, when the road is widened, no additional land is required as the additional lanes can be fit within the existing width of the right of way.

One side effect of the above changes is that the ComponentField struct in the ECS needs to encode an array_len and type_id fields, as well as the additional of a Component kind to the ComponentFieldKind enum. This allows the LinkStyle information to be encoded correctly.

This also meant that instead of just using a single switch on the field.type in the register_component function, we created a get_component_field_kind function, that also returns the array and component information as necessary.

The other big change this week was the creation of a web server within the program. Why create a web-server? Well, firstly, it really makes introspection of the system very easy. If we have the ability to look at an entity’s components and perhaps later even change them, we can debug the program as it is running in real time. Logically, this could also allow a ‘stats’ view on the game when it is running. Wouldn’t it be cool to have a webpage that could be viewed on another device, or another monitor, that shows the population, demand levels, urgent issues that need dealing with, and graphs showing the change in various values.

At the moment, we just have a simple proof of concept web server that can expose the list of all entities, components, and systems. To do this, we are using the zig standard http library. Some say that it is slow and inefficient, but I’ve not noticed that, probably because I’ve incorporated it in to a Thread.Pool for actually processing requests.

pub fn init() !void {
    gpa = std.heap.GeneralPurposeAllocator(.{}){};
    allocator = gpa.allocator();

The init function of the ng.http module, first creates it’s own allocator. Why? Well, we will use this allocator to create the thread pool, and because http connections can be kept alive after a single request has completed, there is no simple way to kill the active threads in the thread pool at program completion. The typical way to use the Thread.Pool is to create a pool, spawn a bunch of tasks to the pool, and then deinit the pool. The problem is that deinit’ing the pool causes all the threads to be joined, which could be a very long time if we have a web browser that just made a request and has kept the connection alive in anticipation of another request about to the made by the user later.

    try pool.init(.{
        .allocator = allocator,
    });

    for (pool.threads) |thread| {
        thread.detach();
    }

We just create the pool, with our local allocator, and then iterate through all the pool’s threads and detach them. This allows the program to exit without having to join the threads.

pub fn deinit() void {
}

The deinit function is empty. Oh no! We are not freeing the memory allocated to the thread pool. Yeah, but we also don’t care that much. The memory allocated to this is very tightly bound, and we trust that the standard library is not leaking memory. Therefore, we can just think ‘yeah, all good here’, not check that the allocator is not leaking, and be happy.

The other block of memory that is allocated at the start is the paths global. This is a `StringHashMap(RequestFunction) that maps a string to a callback function. In the init function, we just add a bunch of paths:

    try paths.put("/", do_index);
    try paths.put("/index.html", do_index);
    try paths.put("/entity", do_entity);
    try paths.put("/component", do_component);
    try paths.put("/system", do_system);

Yeah, that hash map is not free’d either. But it is static for the lifetime of the program.

The web server itself is run in a separate thread.

    http_server_thread = try std.Thread.spawn(.{}, server, .{});
    try http_server_thread.setName("http server");
    http_server_thread.detach();

We detach that thread as well, so that we can just exit whenever we want.

The server function is very simple.

fn server() !void {
    const addr = try std.net.Address.parseIp("127.0.0.1", 4237);
    log.debug("listening at http://{}/ ({})", .{ addr, thread_id });

    var listener = try addr.listen(.{
        .reuse_address = true,
    });

    while (true) {
        const connection = try listener.accept();

        try pool.spawn(process_connection, .{connection});
    }

    listener.deinit();
}

We first create an Address based on parsing an IP address. We use the localhost IPv4 address for security reasons, and the port 4237 which is just a random number.

Then we create a listener, allowing the reuse_address flag to be set. This effectively does a socket, bind, and listen calls.

Then we just enter an endless loop, accepting a connection and spawning the resultant connection off into the thread pool, using the function process_connection. And we have a web-server.


fn process_connection(connection: std.net.Server.Connection) void {
    var buffer: [64 * 1024]u8 = undefined;

    defer connection.stream.close();

    var http_server = std.http.Server.init(connection, &buffer);
    while (true) {
        var request = http_server.receiveHead() catch |err|
            {
                log.err("http request {}", .{err});
                return;
            };

        process_request(&request) catch |err|
            {
                log.err("process request {}", .{err});
                return;
            };
    }
}

The process_connection function inits an http.Server object based on the connection and a static buffer on the stack. This buffer is used to process the headers of a request. 64kB is probably overkill here, but each thread is initialized with 16MB, so I’m not worried about stack overflow here.

We then call the receiveHead function on the http_server. This parses the request header and returns the request. We can then process this request and loop around again. Obviously, if something fails, then we return, closing the connection’s stream as we return.

At this point, we’ve got an http request. We just have to map it to a function that generates an appropriate response. This is where process_request comes in.

fn process_request(request: *std.http.Server.Request) !void {
    number_requests += 1;

    const uri = try std.Uri.parseAfterScheme("http", request.head.target);
    var path_buffer: [256]u8 = undefined;
    const path = std.Uri.percentDecodeBackwards(&path_buffer, uri.path.percent_encoded);

    var response = Response.init(allocator);
    defer response.deinit();

    if (paths.get(path)) |fun| {
        fun(request, &uri, &response);
    } else {
        do_404(request, &uri, &response);
    }

    request.respond(response.buffer.items, .{
        .status = response.status,
        .extra_headers = response.headers.items,
    }) catch |err|
        {
            log.err("request response {}", .{err});
        };

    log.debug("{s} \"{}\" {d:0>3} {} ({})", .{
        @tagName(request.head.method),
        std.zig.fmtEscapes(request.head.target),
        @intFromEnum(response.status),
        response.buffer.items.len,
        std.Thread.getCurrentId(),
    });
}

First we do some accounting, incrementing the number of requests we’ve received. Then we parse the uri of the request.head.target. This is the bit after the GET in the HTTP header. We want the path. The target however is ‘percent encoded’, meaning things like %49 means the character 1. To decode these, we use the percentDecodeBackwards giving it a buffer from the stack.

We have our own Response object, and we create an instance of this. The path that we got from above, we check if it was registered in the paths hash map. If we found a function, we call it with the request, the uri, and the response object. Otherwise, we call a do_404 function with the same parameters.

Finally, we call the request.respond function using the information collected in out response object. And then we log the request.

The target functions are pretty simple.

fn do_404(
    request: *std.http.Server.Request,
    uri: *const std.Uri,
    response: *Response,
) void {
    _ = uri;

    response.set_html();
    response.append(html_header);
    response.append(header_div);
    response.print(http_404_response_text, .{request.head.target});
    response.append(html_footer);
    response.set_status(.not_found);
}

const http_404_response_text =
    \\Invalid request {s}
    \\
;

The do_404 function just says, this is an html response, appending the common html_header, header_div, uses a print function to template the request’s target into the response, and then appends the html_footer. Finally, we set the .not_found status (404). That’s it.

The entity path does a little more than this.

var iter = ng.entity_iterator();
while (iter.next()) |entity| {
    const eid: u32 = @bitCast(entity);
    response.print("<a href=\"/entity?eid={}\">{}</a><br>\n", .{ eid, entity });
}

We have an entity_iterator function exposed by the ecs module that allows us to iterate over all entities. We can then build a list of entities in a list.

Obviously, if we click on one of those links, we have to do something.

var optional_query = parse_query(allocator, uri.query);

if (optional_query) |*query| {
    defer query.deinit();

    if (query.get("eid")) |eid_str| {
        if (parse_int(u32, eid_str)) |eid| {
            const entity: ng.Entity = @bitCast(eid);
            response.print("<b>{}</b><br>\n", .{entity});

            var component_iter = ng.component_iterator();
            while (component_iter.next()) |component| {
                if (component.get_data(entity)) |data| {
                    response.print("<b>{s}</b><br>\n", .{component.name});
                    response.print("{any}<br>\n", .{data});
                }
            }
        }
    }
}

This attempts to parse the uri.query field, which can be optional and therefore returns an optional query object. If we have an query object, then we attempt to get the eid field. If that works, then we parse the string as a u32, and then @bitCast that to an Entity. With that entity information, we can then iterate though all known components, attempting to get_data for that entity for each component. If we have some data, then we can print the component name, and its data.

We could also parse the data returned and print the individual fields of this component data, but that is a project for a later time.

The do_component and do_system functions work in a very similar way.

There are a couple of things that need to be considered before going too far away from this code. Firstly, it might make sense to process these http requests in a system rather than in a separate thread. Logically, we have a bunch of race-conditions in the current code. This is probably something that I will fix next week. This would require a new component that encodes a request and response.

-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Zig                             32           2091           1739          15455
-------------------------------------------------------------------------------

We are up to 15455 lines of code. An additional 885 lines of code for this week. More than last week, but still pretty low. I have been thinking about how the simulation of the world will happen, so I might spend some time working on starting to build this up. The current thoughts are that we have a bunch of entities with the Person component. These have one or move Skills, that encode the skills that a person has. Each person can have say 16 skills and each skill can go from 1 to 255. This means that we need a Skill enumeration for each skill. Obviously, we need to give each person a name, sex, age, and health. I’m sure there will be other components that make sense.

One thing that might be useful here is to have a Random component that is just a random 128-bit integer. We can then take a subset of this value to use for various things. When two persons create a new baby person, their random value would be a random selection of bits from the father and the opposite selection of bits from the mother. If we use these bits to drive visual characteristics like eye color, hair color, height, etc., then we can have children looking similar one or both of their parents.

A Process is just a set of inputs, skills required, and outputs. A person that performs a process would transform the inputs to the outputs based on how close their skill is to 255. If they have a very low skill, then the process may be very inefficient. If it is 255, then it the process would be very efficient. Obviously, as persons perform a process, their skill level for the skills they use will increase. This allows the encoding of apprenticeships, but also education. A person who is at school will learn skills without producing anything.

We can also default the set of skills that a person has based on their Random value.

Obviously, if we have persons that can be educated, we need schools, and they need to have persons attending them. Therefore, we need to have a component that encodes AttendingSchool. If the person is too old to be in education, then they’d have EmployedBy. This obviously then requires that we have School and Company components on entities.

Let’s see if we can build this next week, and then also drive a few new webpages to expose some summary information from the simulation.

Fourth, Game