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, 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, error.Canceled => |e| return e,
else => |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; return;
}, },
}; };
log.info( log.info(
"client from '{f}' has successfully logged into account with uid: {d}", "client from '{f}' has successfully logged into account with uid: {s}",
.{ stream.socket.address, result.uid }, .{ stream.socket.address, result.uid.view() },
); );
world = logic.World.init(&session, assets, result.uid, player, gpa, io); world = logic.World.init(&session, assets, result.uid, player, gpa, io);

View File

@@ -1,6 +1,8 @@
const std = @import("std"); const std = @import("std");
const pb = @import("proto").pb; const pb = @import("proto").pb;
const mem = @import("common").mem;
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const PlayerId = @import("../logic.zig").World.PlayerId;
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator; 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 Error = error{LoginFailed} || Session.SendError || Allocator.Error || Io.Cancelable;
pub const Result = struct { 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 { pub fn processLoginRequest(io: Io, session: *Session, request: *const pb.CS_LOGIN) Error!Result {
log.info("login request received: {any}", .{request}); 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; return error.LoginFailed;
};
try session.send(pb.SC_LOGIN{ try session.send(pb.SC_LOGIN{
.uid = request.uid, .uid = request.uid,
@@ -25,5 +43,5 @@ pub fn processLoginRequest(io: Io, session: *Session, request: *const pb.CS_LOGI
.server_time_zone = 3, .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 Io = std.Io;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Player = logic.Player; const Player = logic.Player;
const PlayerId = logic.World.PlayerId;
const player_data_dir = "store/player/"; const player_data_dir = "store/player/";
const base_component_file = "base_data"; const base_component_file = "base_data";
@@ -35,10 +36,12 @@ const LoadPlayerError = error{
const log = std.log.scoped(.persistence); const log = std.log.scoped(.persistence);
// Opens or creates data directory for the player with specified uid. // Opens or creates data directory for the player with specified uid.
pub fn openPlayerDataDir(io: Io, uid: u64) !Io.Dir { pub fn openPlayerDataDir(io: Io, uid: []const u8) !Io.Dir {
var dir_path_buf: [player_data_dir.len + 20]u8 = undefined; std.debug.assert(uid.len <= PlayerId.max_length);
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. 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(); const cwd: Io.Dir = .cwd();
return cwd.openDir(io, dir_path, .{}) catch |open_err| switch (open_err) { 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. // Loads player data. Creates components that do not exist.
// Resets component to default if its data is corrupted. // 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); const data_dir = try openPlayerDataDir(io, uid);
defer data_dir.close(io); defer data_dir.close(io);
@@ -88,20 +91,20 @@ pub fn loadPlayer(io: Io, gpa: Allocator, assets: *const Assets, uid: u64) !Play
fn loadBaseComponent( fn loadBaseComponent(
io: Io, io: Io,
data_dir: Io.Dir, data_dir: Io.Dir,
uid: u64, uid: []const u8,
) !Player.Base { ) !Player.Base {
return fs.loadStruct(Player.Base, io, data_dir, base_component_file) catch |err| switch (err) { return fs.loadStruct(Player.Base, io, data_dir, base_component_file) catch |err| switch (err) {
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: { inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) { if (e == error.ChecksumMismatch) {
log.err( 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}, .{uid},
); );
} }
if (e == error.ReprSizeMismatch) { if (e == error.ReprSizeMismatch) {
log.err( 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}, .{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; var game_vars: Player.GameVars = undefined;
game_vars.server_vars = try loadArray( 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; 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; var unlock: Player.Unlock = undefined;
unlock.unlocked_systems = try loadArray( unlock.unlocked_systems = try loadArray(
@@ -164,7 +167,7 @@ fn loadUnlockComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Play
return unlock; 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) { const char_bag_dir = data_dir.openDir(io, char_bag_path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.NeedsReset, error.FileNotFound => return error.NeedsReset,
error.Canceled => |e| return e, 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: { inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) { if (e == error.ChecksumMismatch) {
log.err( 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}, .{uid},
); );
} }
if (e == error.ReprSizeMismatch) { if (e == error.ReprSizeMismatch) {
log.err( 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}, .{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) { return fs.loadStruct(Player.Bitset, io, data_dir, bitset_file) catch |err| switch (err) {
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| { inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| {
if (e == error.ChecksumMismatch) { if (e == error.ChecksumMismatch) {
log.err( log.err(
"checksum mismatched for bitset of player {d}, resetting to defaults.", "checksum mismatched for bitset of player {s}, resetting to defaults.",
.{uid}, .{uid},
); );
} }
if (e == error.ReprSizeMismatch) { if (e == error.ReprSizeMismatch) {
log.err( 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}, .{uid},
); );
} }
@@ -448,7 +451,7 @@ fn loadArray(
io: Io, io: Io,
gpa: Allocator, gpa: Allocator,
data_dir: Io.Dir, data_dir: Io.Dir,
uid: u64, uid: []const u8,
sub_path: []const u8, sub_path: []const u8,
defaults: []const T, defaults: []const T,
) ![]T { ) ![]T {
@@ -456,14 +459,14 @@ fn loadArray(
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: { inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| reset: {
if (e == error.ChecksumMismatch) { if (e == error.ChecksumMismatch) {
log.err( 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 }, .{ sub_path, uid },
); );
} }
if (e == error.ReprSizeMismatch) { if (e == error.ReprSizeMismatch) {
log.err( 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 }, .{ sub_path, uid },
); );
} }

View File

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

View File

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