feat(auth): support alphanumeric UIDs

Allows using alphanumeric strings up to 16 characters as UID. Makes it
less clunky to use, since the game suggests alphanumeric one by default.
This commit is contained in:
xeon
2026-02-03 21:35:33 +03:00
parent 277d5f5573
commit 3b20a381fa
5 changed files with 60 additions and 31 deletions

View File

@@ -107,17 +107,20 @@ pub fn process(
error.Canceled => |e| return e,
};
const player = fs.persistence.loadPlayer(io, gpa, assets, result.uid) catch |err| switch (err) {
const player = fs.persistence.loadPlayer(io, gpa, assets, result.uid.view()) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("failed to load data for player with uid {d}: {t}, disconnecting", .{ result.uid, e });
log.err(
"failed to load data for player with uid {s}: {t}, disconnecting",
.{ result.uid.view(), e },
);
return;
},
};
log.info(
"client from '{f}' has successfully logged into account with uid: {d}",
.{ stream.socket.address, result.uid },
"client from '{f}' has successfully logged into account with uid: {s}",
.{ stream.socket.address, result.uid.view() },
);
world = logic.World.init(&session, assets, result.uid, player, gpa, io);

View File

@@ -1,6 +1,8 @@
const std = @import("std");
const pb = @import("proto").pb;
const mem = @import("common").mem;
const Session = @import("../Session.zig");
const PlayerId = @import("../logic.zig").World.PlayerId;
const Io = std.Io;
const Allocator = std.mem.Allocator;
@@ -10,14 +12,30 @@ const log = std.log.scoped(.auth);
pub const Error = error{LoginFailed} || Session.SendError || Allocator.Error || Io.Cancelable;
pub const Result = struct {
uid: u64, // It's a string in SC_LOGIN tho
uid: mem.LimitedString(PlayerId.max_length),
pub const FromUidSliceError = error{
TooLongString,
InvalidCharacters,
};
pub fn fromUidSlice(slice: []const u8) FromUidSliceError!Result {
const result: Result = .{ .uid = try .init(slice) };
for (slice) |c| if (!std.ascii.isAlphanumeric(c)) {
return error.InvalidCharacters;
};
return result;
}
};
pub fn processLoginRequest(io: Io, session: *Session, request: *const pb.CS_LOGIN) Error!Result {
log.info("login request received: {any}", .{request});
const uid = std.fmt.parseInt(u64, request.uid, 10) catch
const result = Result.fromUidSlice(request.uid) catch |err| {
log.err("invalid UID received: {t}", .{err});
return error.LoginFailed;
};
try session.send(pb.SC_LOGIN{
.uid = request.uid,
@@ -25,5 +43,5 @@ pub fn processLoginRequest(io: Io, session: *Session, request: *const pb.CS_LOGI
.server_time_zone = 3,
});
return .{ .uid = uid };
return result;
}

View File

@@ -6,6 +6,7 @@ const Assets = @import("../Assets.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Player = logic.Player;
const PlayerId = logic.World.PlayerId;
const player_data_dir = "store/player/";
const base_component_file = "base_data";
@@ -35,10 +36,12 @@ const LoadPlayerError = error{
const log = std.log.scoped(.persistence);
// Opens or creates data directory for the player with specified uid.
pub fn openPlayerDataDir(io: Io, uid: u64) !Io.Dir {
var dir_path_buf: [player_data_dir.len + 20]u8 = undefined;
const dir_path = std.fmt.bufPrint(&dir_path_buf, player_data_dir ++ "{d}", .{uid}) catch
unreachable; // Since we're printing a u64, it shouldn't exceed the buffer.
pub fn openPlayerDataDir(io: Io, uid: []const u8) !Io.Dir {
std.debug.assert(uid.len <= PlayerId.max_length);
var dir_path_buf: [player_data_dir.len + PlayerId.max_length]u8 = undefined;
const dir_path = std.fmt.bufPrint(&dir_path_buf, player_data_dir ++ "{s}", .{uid}) catch
unreachable;
const cwd: Io.Dir = .cwd();
return cwd.openDir(io, dir_path, .{}) catch |open_err| switch (open_err) {
@@ -53,7 +56,7 @@ pub fn openPlayerDataDir(io: Io, uid: u64) !Io.Dir {
// Loads player data. Creates components that do not exist.
// Resets component to default if its data is corrupted.
pub fn loadPlayer(io: Io, gpa: Allocator, assets: *const Assets, uid: u64) !Player {
pub fn loadPlayer(io: Io, gpa: Allocator, assets: *const Assets, uid: []const u8) !Player {
const data_dir = try openPlayerDataDir(io, uid);
defer data_dir.close(io);
@@ -88,20 +91,20 @@ pub fn loadPlayer(io: Io, gpa: Allocator, assets: *const Assets, uid: u64) !Play
fn loadBaseComponent(
io: Io,
data_dir: Io.Dir,
uid: u64,
uid: []const u8,
) !Player.Base {
return fs.loadStruct(Player.Base, io, data_dir, base_component_file) catch |err| switch (err) {
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) {
log.err(
"checksum mismatched for base_data of player {d}, resetting to defaults.",
"checksum mismatched for base_data of player {s}, resetting to defaults.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for base_data of player {d}, resetting to defaults.",
"struct layout mismatched for base_data of player {s}, resetting to defaults.",
.{uid},
);
}
@@ -116,7 +119,7 @@ fn loadBaseComponent(
};
}
fn loadGameVarsComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Player.GameVars {
fn loadGameVarsComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: []const u8) !Player.GameVars {
var game_vars: Player.GameVars = undefined;
game_vars.server_vars = try loadArray(
@@ -146,7 +149,7 @@ fn loadGameVarsComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Pl
return game_vars;
}
fn loadUnlockComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Player.Unlock {
fn loadUnlockComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: []const u8) !Player.Unlock {
var unlock: Player.Unlock = undefined;
unlock.unlocked_systems = try loadArray(
@@ -164,7 +167,7 @@ fn loadUnlockComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Play
return unlock;
}
fn loadCharBagComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Player.CharBag {
fn loadCharBagComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: []const u8) !Player.CharBag {
const char_bag_dir = data_dir.openDir(io, char_bag_path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.NeedsReset,
error.Canceled => |e| return e,
@@ -195,14 +198,14 @@ fn loadCharBagComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Pla
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) {
log.err(
"checksum mismatched for char bag metadata of player {d}, resetting to defaults.",
"checksum mismatched for char bag metadata of player {s}, resetting to defaults.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for char bag metadata of player {d}, resetting to defaults.",
"struct layout mismatched for char bag metadata of player {s}, resetting to defaults.",
.{uid},
);
}
@@ -398,19 +401,19 @@ pub fn saveCharBagComponent(
}
}
fn loadBitsetComponent(io: Io, data_dir: Io.Dir, uid: u64) !Player.Bitset {
fn loadBitsetComponent(io: Io, data_dir: Io.Dir, uid: []const u8) !Player.Bitset {
return fs.loadStruct(Player.Bitset, io, data_dir, bitset_file) catch |err| switch (err) {
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| {
if (e == error.ChecksumMismatch) {
log.err(
"checksum mismatched for bitset of player {d}, resetting to defaults.",
"checksum mismatched for bitset of player {s}, resetting to defaults.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for bitset of player {d}, resetting to defaults.",
"struct layout mismatched for bitset of player {s}, resetting to defaults.",
.{uid},
);
}
@@ -448,7 +451,7 @@ fn loadArray(
io: Io,
gpa: Allocator,
data_dir: Io.Dir,
uid: u64,
uid: []const u8,
sub_path: []const u8,
defaults: []const T,
) ![]T {
@@ -456,14 +459,14 @@ fn loadArray(
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) {
log.err(
"checksum mismatched for '{s}' of player {d}, resetting to defaults.",
"checksum mismatched for '{s}' of player {s}, resetting to defaults.",
.{ sub_path, uid },
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for '{s}' of player {d}, resetting to defaults.",
"struct layout mismatched for '{s}' of player {s}, resetting to defaults.",
.{ sub_path, uid },
);
}

View File

@@ -1,6 +1,7 @@
// Describes player-local state of the world.
const World = @This();
const std = @import("std");
const mem = @import("common").mem;
const logic = @import("../logic.zig");
const Session = @import("../Session.zig");
const Assets = @import("../Assets.zig");
@@ -8,7 +9,11 @@ const Assets = @import("../Assets.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
pub const PlayerId = struct { uid: u64 };
pub const PlayerId = struct {
pub const max_length: usize = 16;
uid: mem.LimitedString(max_length),
};
player_id: PlayerId,
session: *Session, // TODO: should it be here this way? Do we need an abstraction?
@@ -18,7 +23,7 @@ player: logic.Player,
pub fn init(
session: *Session,
assets: *const Assets,
uid: u64,
uid: mem.LimitedString(PlayerId.max_length),
player: logic.Player,
gpa: Allocator,
io: Io,

View File

@@ -15,12 +15,12 @@ pub fn saveCharBagTeams(
player_id: logic.World.PlayerId,
io: Io,
) !void {
const data_dir = fs.persistence.openPlayerDataDir(io, player_id.uid) catch |err| switch (err) {
const data_dir = fs.persistence.openPlayerDataDir(io, player_id.uid.view()) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err(
"failed to open data dir for player with uid {d}: {t}",
.{ player_id.uid, e },
"failed to open data dir for player with uid {s}: {t}",
.{ player_id.uid.view(), e },
);
return;
},