Initial commit, basic unauthenticated score system
This commit is contained in:
commit
a818d383ec
7 changed files with 596 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# zig build artifacts
|
||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
||||||
|
|
||||||
|
# testing artifacts
|
||||||
|
scores/
|
97
build.zig
Normal file
97
build.zig
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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(.{});
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "revo-scores",
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clap = b.dependency("clap", .{});
|
||||||
|
exe.root_module.addImport("clap", clap.module("clap"));
|
||||||
|
|
||||||
|
const zap = b.dependency("zap", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.openssl = false,
|
||||||
|
});
|
||||||
|
exe.root_module.addImport("zap", zap.module("zap"));
|
||||||
|
|
||||||
|
const lmdb_zig = b.dependency("lmdb-zig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
exe.root_module.addImport("lmdb-zig", lmdb_zig.module("lmdb-zig-mod"));
|
||||||
|
exe.linkLibrary(lmdb_zig.artifact("lmdb-zig"));
|
||||||
|
|
||||||
|
// This declares intent for the executable to be installed into the
|
||||||
|
// standard location when the user invokes the "install" step (the default
|
||||||
|
// step when running `zig build`).
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// This *creates* a Run step in the build graph, to be executed when another
|
||||||
|
// step is evaluated that depends on it. The next line below will establish
|
||||||
|
// such a dependency.
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
|
||||||
|
// By making the run step depend on the install step, it will be run from the
|
||||||
|
// installation directory rather than directly from within the cache directory.
|
||||||
|
// This is not necessary, however, if the application depends on other installed
|
||||||
|
// files, this ensures they will be present and in the expected location.
|
||||||
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
|
|
||||||
|
// This allows the user to pass arguments to the application in the build
|
||||||
|
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||||
|
// and can be selected like this: `zig build run`
|
||||||
|
// This will evaluate the `run` step rather than the default, which is "install".
|
||||||
|
const run_step = b.step("run", "Run the app");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
const exe_unit_tests = b.addTest(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_exe_unit_tests = b.addRunArtifact(exe_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_exe_unit_tests.step);
|
||||||
|
|
||||||
|
// make zls work better
|
||||||
|
const exe_check = b.addExecutable(.{
|
||||||
|
.name = "revo-scores",
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
exe_check.root_module.addImport("zap", zap.module("zap"));
|
||||||
|
exe_check.root_module.addImport("lmdb-zig", lmdb_zig.module("lmdb-zig-mod"));
|
||||||
|
exe_check.root_module.addImport("clap", clap.module("clap"));
|
||||||
|
|
||||||
|
const check = b.step("check", "Check if exe compiles");
|
||||||
|
check.dependOn(&exe_check.step);
|
||||||
|
}
|
47
build.zig.zon
Normal file
47
build.zig.zon
Normal 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 = "revo-scores",
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
|
||||||
|
// This field is optional.
|
||||||
|
// This is currently advisory only; Zig does not yet do anything
|
||||||
|
// with this value.
|
||||||
|
//.minimum_zig_version = "0.11.0",
|
||||||
|
|
||||||
|
// 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 = .{
|
||||||
|
.zap = .{
|
||||||
|
.url = "git+https://github.com/zigzap/zap?ref=v0.9.0#9543ede15fc9ad038ec4839628c886b124580983",
|
||||||
|
.hash = "1220bb12de9055d585ee0b19c8c9da41ad49a660f5da8be1372c7b85ba43f6e47372",
|
||||||
|
},
|
||||||
|
.@"lmdb-zig" = .{
|
||||||
|
.url = "git+https://github.com/john-g4lt/lmdb-zig?ref=v0.1.0#3ec55b07424ed1f4faba18a6d7d7b4360936d4c5",
|
||||||
|
.hash = "1220a44f2880d150589664ee936da3b856674d732cbf02b4714a7282fa752facad97",
|
||||||
|
},
|
||||||
|
.clap = .{
|
||||||
|
.url = "git+https://github.com/hejsil/zig-clap?ref=0.9.1#d71cc39a94f3e6ccbad00c25d350c9147de4df9f",
|
||||||
|
.hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
// For example...
|
||||||
|
//"LICENSE",
|
||||||
|
//"README.md",
|
||||||
|
},
|
||||||
|
}
|
9
config.json
Normal file
9
config.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"listen_port": 3000,
|
||||||
|
"db_path": "scores/",
|
||||||
|
"levels": [
|
||||||
|
"hills",
|
||||||
|
"canopy",
|
||||||
|
"mountain"
|
||||||
|
]
|
||||||
|
}
|
68
src/main.zig
Normal file
68
src/main.zig
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const clap = @import("clap");
|
||||||
|
const zap = @import("zap");
|
||||||
|
const lmdb = @import("lmdb-zig");
|
||||||
|
|
||||||
|
const Scores = @import("scores.zig");
|
||||||
|
const ScoreWeb = @import("scoreweb.zig");
|
||||||
|
|
||||||
|
/// fallback request handler
|
||||||
|
fn onRequest(r: zap.Request) void {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.sendBody("Invalid path!") catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Config = struct {
|
||||||
|
listen_port: usize,
|
||||||
|
db_path: []const u8,
|
||||||
|
levels: []const []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||||
|
.thread_safe = true,
|
||||||
|
}){};
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
// scope everything so we can detect leaks.
|
||||||
|
{
|
||||||
|
// load config file
|
||||||
|
var file = try std.fs.cwd().openFile("config.json", .{});
|
||||||
|
defer file.close();
|
||||||
|
const json_str = try file.readToEndAlloc(allocator, 2048);
|
||||||
|
defer allocator.free(json_str);
|
||||||
|
const cfg_v = try json.parseFromSlice(Config, allocator, json_str, .{ .ignore_unknown_fields = true });
|
||||||
|
defer cfg_v.deinit();
|
||||||
|
const config = cfg_v.value;
|
||||||
|
|
||||||
|
// set up HTTP listener
|
||||||
|
var listener = zap.Endpoint.Listener.init(
|
||||||
|
allocator,
|
||||||
|
.{
|
||||||
|
.port = config.listen_port,
|
||||||
|
.on_request = onRequest,
|
||||||
|
.max_clients = 100_000,
|
||||||
|
.max_body_size = 100 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
defer listener.deinit();
|
||||||
|
|
||||||
|
var score_web = try ScoreWeb.init(allocator, "/api/scores", config);
|
||||||
|
defer score_web.deinit();
|
||||||
|
|
||||||
|
try listener.register(score_web.endpoint());
|
||||||
|
|
||||||
|
try listener.listen();
|
||||||
|
std.debug.print("Listening on 0.0.0.0:{d}\n", .{config.listen_port});
|
||||||
|
|
||||||
|
zap.start(.{
|
||||||
|
.threads = 2,
|
||||||
|
.workers = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const has_leaked = gpa.detectLeaks();
|
||||||
|
std.log.debug("Has leaked: {}\n", .{has_leaked});
|
||||||
|
}
|
147
src/scores.zig
Normal file
147
src/scores.zig
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const json = std.json;
|
||||||
|
const lmdb = @import("lmdb-zig");
|
||||||
|
const pwhash = std.crypto.pwhash;
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
env: lmdb.Env,
|
||||||
|
players: lmdb.Db,
|
||||||
|
levels: std.StringHashMap(lmdb.Db),
|
||||||
|
|
||||||
|
pub const Self = @This();
|
||||||
|
|
||||||
|
const ScoreAccessError = error{
|
||||||
|
LevelNotfound,
|
||||||
|
NoScoreForPlayer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Difficulty = enum(u8) {
|
||||||
|
sweet,
|
||||||
|
salty,
|
||||||
|
spicy,
|
||||||
|
pungent,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const Difficulty, jw: anytype) !void {
|
||||||
|
const n: u8 = @intFromEnum(self.*);
|
||||||
|
try jw.write(n);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Score = packed struct {
|
||||||
|
score: u64 = 0,
|
||||||
|
time: u64 = 0,
|
||||||
|
difficulty: Difficulty = .salty,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, db_path: []const u8, level_ids: []const []const u8) !Self {
|
||||||
|
const env = try lmdb.Env.init(db_path, .{
|
||||||
|
.max_num_dbs = level_ids.len + 1,
|
||||||
|
});
|
||||||
|
errdefer env.deinit();
|
||||||
|
|
||||||
|
const tx = try env.begin(.{});
|
||||||
|
|
||||||
|
const players = try tx.open("players", .{ .create_if_not_exists = true });
|
||||||
|
|
||||||
|
var levels = std.StringHashMap(lmdb.Db).init(allocator);
|
||||||
|
errdefer levels.deinit();
|
||||||
|
|
||||||
|
errdefer tx.deinit();
|
||||||
|
for (level_ids) |lvl| {
|
||||||
|
if (levels.contains(lvl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db_name = try std.mem.concatWithSentinel(allocator, u8, &.{ "level/", lvl }, 0);
|
||||||
|
defer allocator.free(db_name);
|
||||||
|
|
||||||
|
const db = try tx.open(db_name, .{ .create_if_not_exists = true });
|
||||||
|
errdefer db.close(env);
|
||||||
|
try levels.put(lvl, db);
|
||||||
|
}
|
||||||
|
try tx.commit();
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.allocator = allocator,
|
||||||
|
.env = env,
|
||||||
|
.players = players,
|
||||||
|
.levels = levels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.levels.deinit();
|
||||||
|
|
||||||
|
self.players.close(self.env);
|
||||||
|
|
||||||
|
self.env.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a player's score for the given level.
|
||||||
|
pub fn getScore(self: *Self, level: []const u8, player: []const u8) !Score {
|
||||||
|
const tx = try self.env.begin(.{ .read_only = true });
|
||||||
|
errdefer tx.deinit();
|
||||||
|
const db = self.levels.get(level) orelse {
|
||||||
|
return error.LevelNotfound;
|
||||||
|
};
|
||||||
|
const v = tx.get(db, player) catch |err| switch (err) {
|
||||||
|
error.not_found => return error.NoScoreForPlayer,
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
const score = std.mem.bytesToValue(Score, v);
|
||||||
|
try tx.commit();
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a player's score for the given level.
|
||||||
|
pub fn setScore(self: *Self, level: []const u8, player: []const u8, score: Score) !void {
|
||||||
|
const tx = try self.env.begin(.{});
|
||||||
|
errdefer tx.deinit();
|
||||||
|
const db = self.levels.get(level) orelse {
|
||||||
|
return error.LevelNotFound;
|
||||||
|
};
|
||||||
|
try tx.put(db, player, std.mem.asBytes(&score), .{});
|
||||||
|
try tx.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a player's score for the given level.
|
||||||
|
pub fn deleteScore(self: *Self, level: []const u8, player: []const u8) !void {
|
||||||
|
const tx = try self.env.begin(.{});
|
||||||
|
errdefer tx.deinit();
|
||||||
|
const db = self.levels.get(level) orelse {
|
||||||
|
return error.LevelNotFound;
|
||||||
|
};
|
||||||
|
try tx.del(db, player, .key);
|
||||||
|
try tx.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listLevels(self: *Self) !std.ArrayList([]const u8) {
|
||||||
|
var levels = try std.ArrayList([]const u8).initCapacity(self.allocator, self.levels.count());
|
||||||
|
errdefer levels.deinit();
|
||||||
|
|
||||||
|
var lvl_iter = self.levels.keyIterator();
|
||||||
|
while (lvl_iter.next()) |lvl| {
|
||||||
|
try levels.append(lvl.*);
|
||||||
|
}
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listScores(self: *Self, level: []const u8) !std.StringArrayHashMap(Score) {
|
||||||
|
var scores = std.StringArrayHashMap(Score).init(self.allocator);
|
||||||
|
errdefer scores.deinit();
|
||||||
|
|
||||||
|
const tx = try self.env.begin(.{ .read_only = true });
|
||||||
|
errdefer tx.deinit();
|
||||||
|
const db = self.levels.get(level) orelse {
|
||||||
|
return error.LevelNotfound;
|
||||||
|
};
|
||||||
|
|
||||||
|
var cursor = try tx.cursor(db);
|
||||||
|
while (try cursor.next_key()) |entry| {
|
||||||
|
const score = std.mem.bytesToValue(Score, entry.val);
|
||||||
|
try scores.put(entry.key, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
try tx.commit();
|
||||||
|
return scores;
|
||||||
|
}
|
222
src/scoreweb.zig
Normal file
222
src/scoreweb.zig
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const zap = @import("zap");
|
||||||
|
|
||||||
|
const Scores = @import("scores.zig");
|
||||||
|
const Config = @import("main.zig").Config;
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
parent_path_parts: usize,
|
||||||
|
scores: Scores,
|
||||||
|
ep: zap.Endpoint,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, path: []const u8, config: Config) !Self {
|
||||||
|
return Self{
|
||||||
|
.allocator = allocator,
|
||||||
|
.scores = try Scores.init(allocator, config.db_path, config.levels),
|
||||||
|
.parent_path_parts = blk: {
|
||||||
|
var parts: usize = 0;
|
||||||
|
var split = std.mem.splitScalar(u8, path, '/');
|
||||||
|
while (split.next()) |_| {
|
||||||
|
parts += 1;
|
||||||
|
}
|
||||||
|
break :blk parts;
|
||||||
|
},
|
||||||
|
.ep = zap.Endpoint.init(.{
|
||||||
|
.path = path,
|
||||||
|
.get = getScores,
|
||||||
|
.post = postScore,
|
||||||
|
.delete = deleteScore,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.scores.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endpoint(self: *Self) *zap.Endpoint {
|
||||||
|
return &self.ep;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partsOfPath(self: *Self, path: []const u8) !std.ArrayList([]const u8) {
|
||||||
|
var parts = std.ArrayList([]const u8).init(self.allocator);
|
||||||
|
errdefer parts.deinit();
|
||||||
|
var split = std.mem.splitScalar(u8, path, '/');
|
||||||
|
var n: usize = 0;
|
||||||
|
while (split.next()) |part| {
|
||||||
|
if (n >= self.parent_path_parts and part.len > 0) {
|
||||||
|
try parts.append(part);
|
||||||
|
}
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serverError(r: zap.Request, body: []const u8) void {
|
||||||
|
r.setStatus(.internal_server_error);
|
||||||
|
r.sendBody(body) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notFound(r: zap.Request, body: []const u8) void {
|
||||||
|
r.setStatus(.not_found);
|
||||||
|
r.sendBody(body) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn badRequest(r: zap.Request, body: []const u8) void {
|
||||||
|
r.setStatus(.bad_request);
|
||||||
|
r.sendBody(body) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getScores(e: *zap.Endpoint, r: zap.Request) void {
|
||||||
|
const self: *Self = @fieldParentPtr("ep", e);
|
||||||
|
|
||||||
|
if (r.path) |path| {
|
||||||
|
const parts = self.partsOfPath(path) catch {
|
||||||
|
return serverError(r, "Path processing error.");
|
||||||
|
};
|
||||||
|
defer parts.deinit();
|
||||||
|
|
||||||
|
switch (parts.items.len) {
|
||||||
|
0 => return self.listLevels(r),
|
||||||
|
1 => return self.listScores(r, parts.items[0]),
|
||||||
|
2 => return self.getScore(r, parts.items[0], parts.items[1]),
|
||||||
|
else => return badRequest(r, "Invalid request."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn postScore(e: *zap.Endpoint, r: zap.Request) void {
|
||||||
|
const self: *Self = @fieldParentPtr("ep", e);
|
||||||
|
|
||||||
|
if (r.path) |path| {
|
||||||
|
const parts = self.partsOfPath(path) catch {
|
||||||
|
return serverError(r, "Path processing error.");
|
||||||
|
};
|
||||||
|
defer parts.deinit();
|
||||||
|
|
||||||
|
if (parts.items.len != 5) {
|
||||||
|
return badRequest(r, "Invalid request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = parts.items[0];
|
||||||
|
const player = parts.items[1];
|
||||||
|
const score = parts.items[2];
|
||||||
|
const time = parts.items[3];
|
||||||
|
const difficulty = parts.items[4];
|
||||||
|
|
||||||
|
const new_score = Scores.Score{
|
||||||
|
.score = std.fmt.parseInt(u64, score, 10) catch {
|
||||||
|
return badRequest(r, "Invalid score value.");
|
||||||
|
},
|
||||||
|
.time = std.fmt.parseInt(u64, time, 10) catch {
|
||||||
|
return badRequest(r, "Invalid time value.");
|
||||||
|
},
|
||||||
|
.difficulty = blk: {
|
||||||
|
const n = std.fmt.parseInt(u8, difficulty, 10) catch {
|
||||||
|
return badRequest(r, "Invalid difficulty value.");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (n > @intFromEnum(Scores.Difficulty.pungent)) {
|
||||||
|
return badRequest(r, "Invalid difficulty value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @enumFromInt(n);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// const old_score = switch (self.scores.getScore(level, player)) {
|
||||||
|
// .ok => |s| s,
|
||||||
|
// .err => |err| switch (err) {
|
||||||
|
// error.NoScoreForPlayer => Scores.Score{},
|
||||||
|
// error.LevelNotfound => return notFound(r, "Level not found."),
|
||||||
|
// else => return serverError(r, "Error getting score."),
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
const old_score = self.scores.getScore(level, player) catch |err| switch (err) {
|
||||||
|
error.NoScoreForPlayer => {
|
||||||
|
self.scores.setScore(level, player, new_score) catch {
|
||||||
|
return serverError(r, "Error setting score.");
|
||||||
|
};
|
||||||
|
r.setStatus(.accepted);
|
||||||
|
return r.sendBody("Score set!") catch return;
|
||||||
|
},
|
||||||
|
error.LevelNotfound => return notFound(r, "Level not found."),
|
||||||
|
else => return serverError(r, "Error getting score."),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (new_score.score < old_score.score) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new_score.score == old_score.score) {
|
||||||
|
if (@intFromEnum(new_score.difficulty) < @intFromEnum(old_score.difficulty)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new_score.time >= old_score.time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scores.setScore(level, player, new_score) catch {
|
||||||
|
return serverError(r, "Error setting score.");
|
||||||
|
};
|
||||||
|
r.setStatus(.accepted);
|
||||||
|
return r.sendBody("Score set!") catch return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deleteScore(e: *zap.Endpoint, r: zap.Request) void {
|
||||||
|
_ = e;
|
||||||
|
_ = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listLevels(self: *Self, r: zap.Request) void {
|
||||||
|
const levels = self.scores.listLevels() catch {
|
||||||
|
return serverError(r, "Error listing levels.");
|
||||||
|
};
|
||||||
|
defer levels.deinit();
|
||||||
|
|
||||||
|
const str = json.stringifyAlloc(self.allocator, levels.items, .{}) catch {
|
||||||
|
return serverError(r, "Error listing levels.");
|
||||||
|
};
|
||||||
|
defer self.allocator.free(str);
|
||||||
|
|
||||||
|
r.sendJson(str) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listScores(self: *Self, r: zap.Request, level: []const u8) void {
|
||||||
|
var scores = self.scores.listScores(level) catch |err| switch (err) {
|
||||||
|
error.LevelNotfound => return notFound(r, "Level not found."),
|
||||||
|
else => return serverError(r, "Error listing scores."),
|
||||||
|
};
|
||||||
|
defer scores.deinit();
|
||||||
|
|
||||||
|
var scores_json = json.ArrayHashMap(Scores.Score){ .map = scores.unmanaged };
|
||||||
|
var str = std.ArrayList(u8).init(self.allocator);
|
||||||
|
defer str.deinit();
|
||||||
|
var write_stream = json.writeStream(str.writer(), .{});
|
||||||
|
scores_json.jsonStringify(&write_stream) catch {
|
||||||
|
return serverError(r, "Error listing scores.");
|
||||||
|
};
|
||||||
|
|
||||||
|
r.sendJson(str.items) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getScore(self: *Self, r: zap.Request, level: []const u8, player: []const u8) void {
|
||||||
|
const score = self.scores.getScore(level, player) catch |err| switch (err) {
|
||||||
|
error.LevelNotfound => return notFound(r, "Level not found."),
|
||||||
|
error.NoScoreForPlayer => return notFound(r, "No score for player."),
|
||||||
|
else => return serverError(r, "Error getting score."),
|
||||||
|
};
|
||||||
|
|
||||||
|
const str = json.stringifyAlloc(self.allocator, score, .{}) catch {
|
||||||
|
return serverError(r, "Error getting score.");
|
||||||
|
};
|
||||||
|
defer self.allocator.free(str);
|
||||||
|
|
||||||
|
r.sendJson(str) catch return;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue