Compare commits

11 Commits

Author SHA1 Message Date
xeon
23f9797a8a feat(scene): implement saving for scene component
Keep in mind that the movement itself isn't acknowledged by the server yet.
However, teleport transitions are now persistent.
2026-02-04 15:38:49 +03:00
xeon
66dff8ddb1 refactor(scene): factor out player location into a persistent component 2026-02-04 15:32:04 +03:00
xeon
3acc274a7d chore(build!): update to zig 0.16.0-dev.2471+e9eadee00 2026-02-04 14:55:46 +03:00
xeon
f0c3f57602 feat(scene): implement basic teleport functionality
Currently without 'unlocking' the teleports themselves. Players can get
teleported by 'tracking' teleport's location on the map.
Player position persistence is not implemented yet.
2026-02-04 01:12:04 +03:00
xeon
5dff70b504 chore(Assets): add TeleportValidationDataTable 2026-02-04 01:00:29 +03:00
xeon
f96fb51e3c refactor(scene): decouple player level/position from notifying systems 2026-02-04 00:45:37 +03:00
xeon
7a91e1e975 chore(Assets): add LevelMapMark config 2026-02-04 00:25:47 +03:00
xeon
b5012c03f4 chore(Tables): add TrackMapPointTable 2026-02-03 23:55:58 +03:00
xeon
3b20a381fa 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.
2026-02-03 21:35:42 +03:00
xeon
277d5f5573 chore(build!): update to zig 0.16.0-dev.2459+37d14a4f3 2026-02-03 21:10:34 +03:00
xeon
104fcf4b95 docs(readme): clarify debug login usage 2026-02-03 03:06:29 +03:00
24 changed files with 51880 additions and 67 deletions

View File

@@ -4,7 +4,7 @@
# Getting Started # Getting Started
## Requirements ## Requirements
- Zig 0.16.0-dev.2368: [Linux](https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.2368+380ea6fb5.tar.xz)/[Windows](https://ziglang.org/builds/zig-x86_64-windows-0.16.0-dev.2368+380ea6fb5.zip) - Zig 0.16.0-dev.2459: [Linux](https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.2459+37d14a4f3.tar.xz)/[Windows](https://ziglang.org/builds/zig-x86_64-windows-0.16.0-dev.2459+37d14a4f3.zip)
#### For additional help, you can join our [discord server](https://discord.xeondev.com) #### For additional help, you can join our [discord server](https://discord.xeondev.com)
@@ -33,6 +33,8 @@ Currently supported client version is `1.0.14`, you can get it from 3rd party so
Next, you have to apply the necessary [client patch](https://git.xeondev.com/LR/C). It allows you to connect to the local server. Next, you have to apply the necessary [client patch](https://git.xeondev.com/LR/C). It allows you to connect to the local server.
Once the patched game is running, you'll see a debug login window with a bunch of random characters. You should replace these with some numeric UID (digits only). After that, you can login normally.
## Community ## Community
- [Our Discord Server](https://discord.xeondev.com) - [Our Discord Server](https://discord.xeondev.com)
- [Our Telegram Channel](https://t.me/reversedrooms) - [Our Telegram Channel](https://t.me/reversedrooms)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
[
{
"Key": "1",
"Value": {
"end": "map01_lv002",
"pos": {
"x": -660.3,
"y": 119.32,
"z": -126.9
},
"quest": "",
"start": "map01_lv001"
}
},
{
"Key": "24",
"Value": {
"end": "map02_lv004",
"pos": {
"x": 118.47,
"y": 137.65,
"z": 0.5
},
"quest": "",
"start": "indie_dg007"
}
},
{
"Key": "11",
"Value": {
"end": "map02_lv002",
"pos": {
"x": -961.8,
"y": 263.46,
"z": -895.33
},
"quest": "e6m5_q#3",
"start": "map02_lv001"
}
},
{
"Key": "15",
"Value": {
"end": "map02_lv002",
"pos": {
"x": -1711.17,
"y": 408.86,
"z": -338.58
},
"quest": "",
"start": "indie_dg005"
}
},
{
"Key": "3",
"Value": {
"end": "map01_lv005",
"pos": {
"x": 255.0,
"y": 99.0,
"z": -340.0
},
"quest": "",
"start": "map01_lv001"
}
},
{
"Key": "14",
"Value": {
"end": "map02_lv001",
"pos": {
"x": -718.6,
"y": 256.91,
"z": -896.89
},
"quest": "",
"start": "map02_lv002"
}
},
{
"Key": "2",
"Value": {
"end": "map01_lv003",
"pos": {
"x": 129.0,
"y": 73.0,
"z": -567.0
},
"quest": "",
"start": "map01_lv001"
}
},
{
"Key": "16",
"Value": {
"end": "indie_dg005",
"pos": {
"x": -1443.57,
"y": 324.2216,
"z": -336.5857
},
"quest": "",
"start": "map02_lv002"
}
},
{
"Key": "25",
"Value": {
"end": "map02_lv001",
"pos": {
"x": -1151.73,
"y": 206.17,
"z": -1355.25
},
"quest": "",
"start": "map02_lv004"
}
},
{
"Key": "13",
"Value": {
"end": "map02_lv002",
"pos": {
"x": -719.07,
"y": 257.85,
"z": -895.02
},
"quest": "",
"start": "map02_lv001"
}
},
{
"Key": "9",
"Value": {
"end": "map01_lv007",
"pos": {
"x": 127.0,
"y": 93.5,
"z": 711.0
},
"quest": "",
"start": "map01_lv006"
}
},
{
"Key": "23",
"Value": {
"end": "indie_dg007",
"pos": {
"x": -1087.61,
"y": 38.05,
"z": -864.19
},
"quest": "",
"start": "map02_lv004"
}
},
{
"Key": "8",
"Value": {
"end": "map01_lv005",
"pos": {
"x": 682.0,
"y": 54.0,
"z": -1.0
},
"quest": "",
"start": "map01_lv006"
}
},
{
"Key": "19",
"Value": {
"end": "map02_lv004",
"pos": {
"x": -1384.38,
"y": 276.66,
"z": -896.44
},
"quest": "",
"start": "map02_lv002"
}
},
{
"Key": "17",
"Value": {
"end": "map02_lv003",
"pos": {
"x": -529.4,
"y": 262.25,
"z": -1024.36
},
"quest": "",
"start": "map02_lv001"
}
},
{
"Key": "10",
"Value": {
"end": "map01_lv006",
"pos": {
"x": 129.0,
"y": 93.5,
"z": 714.0
},
"quest": "",
"start": "map01_lv007"
}
},
{
"Key": "20",
"Value": {
"end": "map02_lv002",
"pos": {
"x": -1383.36,
"y": 278.66,
"z": -895.34
},
"quest": "",
"start": "map02_lv004"
}
},
{
"Key": "18",
"Value": {
"end": "map02_lv001",
"pos": {
"x": -529.7,
"y": 262.25,
"z": -1023.57
},
"quest": "",
"start": "map02_lv003"
}
},
{
"Key": "22",
"Value": {
"end": "map02_lv002",
"pos": {
"x": -640.41,
"y": 253.97,
"z": -326.63
},
"quest": "",
"start": "map02_lv005"
}
},
{
"Key": "5",
"Value": {
"end": "map01_lv001",
"pos": {
"x": 127.0,
"y": 73.0,
"z": -567.0
},
"quest": "",
"start": "map01_lv003"
}
},
{
"Key": "4",
"Value": {
"end": "map01_lv001",
"pos": {
"x": -656.74,
"y": 119.62,
"z": -129.1
},
"quest": "",
"start": "map01_lv002"
}
},
{
"Key": "21",
"Value": {
"end": "map02_lv005",
"pos": {
"x": -639.63,
"y": 253.97,
"z": -326.32
},
"quest": "",
"start": "map02_lv002"
}
},
{
"Key": "7",
"Value": {
"end": "map01_lv006",
"pos": {
"x": 682.0,
"y": 54.0,
"z": 1.0
},
"quest": "",
"start": "map01_lv005"
}
},
{
"Key": "12",
"Value": {
"end": "map02_lv001",
"pos": {
"x": -961.8,
"y": 264.13,
"z": -897.53
},
"quest": "e6m5_q#3",
"start": "map02_lv002"
}
},
{
"Key": "6",
"Value": {
"end": "map01_lv001",
"pos": {
"x": 257.0,
"y": 99.0,
"z": -341.0
},
"quest": "",
"start": "map01_lv005"
}
}
]

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .LR_S, .name = .LR_S,
.version = "0.1.0", .version = "0.1.0",
.minimum_zig_version = "0.16.0-dev.2368+380ea6fb5", .minimum_zig_version = "0.16.0-dev.2471+e9eadee00",
.paths = .{""}, .paths = .{""},
.fingerprint = 0x50ff8392fab61337, .fingerprint = 0x50ff8392fab61337,
} }

View File

@@ -1721,16 +1721,6 @@ pub fn Poll(comptime io_options: IoOptions) type {
return tio.vtable.dirCreateDirPathOpen(tio.userdata, dir, sub_path, perm, options); return tio.vtable.dirCreateDirPathOpen(tio.userdata, dir, sub_path, perm, options);
} }
fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, header: []const u8, data: []const []const u8, splat: usize) Io.File.Writer.Error!usize {
const p: *ThisPoll = @ptrCast(@alignCast(userdata));
try checkCancel(p);
var t: Io.Threaded = .init_single_threaded;
const tio = t.io();
return tio.vtable.fileWriteStreaming(tio.userdata, file, header, data, splat);
}
fn fileWritePositional(userdata: ?*anyopaque, file: Io.File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) Io.File.WritePositionalError!usize { fn fileWritePositional(userdata: ?*anyopaque, file: Io.File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) Io.File.WritePositionalError!usize {
const p: *ThisPoll = @ptrCast(@alignCast(userdata)); const p: *ThisPoll = @ptrCast(@alignCast(userdata));
try checkCancel(p); try checkCancel(p);
@@ -1791,14 +1781,16 @@ pub fn Poll(comptime io_options: IoOptions) type {
return tio.vtable.fileReadPositional(tio.userdata, file, data, offset); return tio.vtable.fileReadPositional(tio.userdata, file, data, offset);
} }
fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: []const []u8) Io.File.Reader.Error!usize { fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Operation.Result {
// TODO: implement network operations once they're migrated to this API.
const p: *ThisPoll = @ptrCast(@alignCast(userdata)); const p: *ThisPoll = @ptrCast(@alignCast(userdata));
try checkCancel(p); try checkCancel(p);
var t: Io.Threaded = .init_single_threaded; var t: Io.Threaded = .init_single_threaded;
const tio = t.io(); const tio = t.io();
return tio.vtable.fileReadStreaming(tio.userdata, file, data); return tio.vtable.operate(tio.userdata, operation);
} }
fn socketClose(socket: posix.socket_t) void { fn socketClose(socket: posix.socket_t) void {

2
envrc
View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# DEPENDS: curl, tar, xz, realpath # DEPENDS: curl, tar, xz, realpath
ZIG_VERSION="0.16.0-dev.2368+380ea6fb5" ZIG_VERSION="0.16.0-dev.2471+e9eadee00"
ZIG_PLATFORM="x86_64-linux" ZIG_PLATFORM="x86_64-linux"
ZIG_DIST="zig-${ZIG_PLATFORM}-${ZIG_VERSION}" ZIG_DIST="zig-${ZIG_PLATFORM}-${ZIG_VERSION}"
ZIG_DIR="./.direnv/${ZIG_DIST}/" ZIG_DIR="./.direnv/${ZIG_DIST}/"

View File

@@ -7,6 +7,7 @@ pub const CharacterSkillMap = @import("Assets/CharacterSkillMap.zig");
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const StringHashMap = std.StringArrayHashMapUnmanaged;
const meta = std.meta; const meta = std.meta;
const log = std.log.scoped(.assets); const log = std.log.scoped(.assets);
@@ -17,7 +18,12 @@ char_skill_map: CharacterSkillMap,
str_to_num_dicts: IndexDictionaries.StrToNum, str_to_num_dicts: IndexDictionaries.StrToNum,
num_to_str_dicts: IndexDictionaries.NumToStr, num_to_str_dicts: IndexDictionaries.NumToStr,
common_skill_config: configs.CommonSkillConfig, common_skill_config: configs.CommonSkillConfig,
level_config_table: std.StringArrayHashMapUnmanaged(configs.LevelConfig), level_config_table: StringHashMap(configs.LevelConfig),
// Map mark groups as they're stored in LevelMapMark.json
level_map_mark_groups: StringHashMap([]const configs.ClientSingleMapMarkData),
// instId-to-data mapping
map_mark_table: StringHashMap(*const configs.ClientSingleMapMarkData),
teleport_validation_table: configs.TeleportValidationDataTable,
pub const IdGroup = enum { pub const IdGroup = enum {
char_id, char_id,
@@ -63,11 +69,27 @@ pub fn load(io: Io, gpa: Allocator) !Assets {
configs.CommonSkillConfig.file, configs.CommonSkillConfig.file,
); );
const level_config_table = try configs.loadJsonConfig( const level_config_table = (try configs.loadJsonConfig(
std.json.ArrayHashMap(configs.LevelConfig), std.json.ArrayHashMap(configs.LevelConfig),
io, io,
arena.allocator(), arena.allocator(),
"LevelConfigTable.json", "LevelConfigTable.json",
)).map;
const level_map_mark_groups = (try configs.loadJsonConfig(
std.json.ArrayHashMap([]const configs.ClientSingleMapMarkData),
io,
arena.allocator(),
"LevelMapMark.json",
)).map;
const map_mark_table = try buildMapMarkTable(&level_map_mark_groups, arena.allocator());
const teleport_validation_table = try configs.loadJsonConfig(
configs.TeleportValidationDataTable,
io,
arena.allocator(),
"MapTeleportValidationDataTable.json",
); );
return .{ return .{
@@ -77,10 +99,31 @@ pub fn load(io: Io, gpa: Allocator) !Assets {
.str_to_num_dicts = str_to_num_dicts, .str_to_num_dicts = str_to_num_dicts,
.num_to_str_dicts = num_to_str_dicts, .num_to_str_dicts = num_to_str_dicts,
.common_skill_config = common_skill_config, .common_skill_config = common_skill_config,
.level_config_table = level_config_table.map, .level_config_table = level_config_table,
.level_map_mark_groups = level_map_mark_groups,
.map_mark_table = map_mark_table,
.teleport_validation_table = teleport_validation_table,
}; };
} }
fn buildMapMarkTable(
groups: *const StringHashMap([]const configs.ClientSingleMapMarkData),
arena: Allocator,
) Allocator.Error!StringHashMap(*const configs.ClientSingleMapMarkData) {
var map: StringHashMap(*const configs.ClientSingleMapMarkData) = .empty;
for (groups.values()) |group| for (group) |*mark| {
const inst_id = try std.mem.concat(
arena,
u8,
&.{ mark.basicData.templateId, mark.basicData.markInstId },
);
try map.put(arena, inst_id, mark);
};
return map;
}
pub fn deinit(assets: *Assets) void { pub fn deinit(assets: *Assets) void {
assets.owned_tables.deinit(); assets.owned_tables.deinit();
assets.arena.deinit(); assets.arena.deinit();

View File

@@ -14,6 +14,7 @@ pub const SkillPatchDataBundleList = @import("Tables/SkillPatchDataBundleList.zi
pub const WeaponBasicData = @import("Tables/WeaponBasicData.zig"); pub const WeaponBasicData = @import("Tables/WeaponBasicData.zig");
pub const CharWpnRecommendData = @import("Tables/CharWpnRecommendData.zig"); pub const CharWpnRecommendData = @import("Tables/CharWpnRecommendData.zig");
pub const DomainData = @import("Tables/DomainData.zig"); pub const DomainData = @import("Tables/DomainData.zig");
pub const MapPointData = @import("Tables/MapPointData.zig");
pub const StrToNum = struct { pub const StrToNum = struct {
pub const file = "StrIdNumTable.json"; pub const file = "StrIdNumTable.json";
@@ -32,6 +33,7 @@ str_to_num: StringArrayHashMap(StrToNum),
num_to_str: StringArrayHashMap(NumToStr), num_to_str: StringArrayHashMap(NumToStr),
char_wpn_recommend: StringArrayHashMap(CharWpnRecommendData), char_wpn_recommend: StringArrayHashMap(CharWpnRecommendData),
domain_data: StringArrayHashMap(DomainData), domain_data: StringArrayHashMap(DomainData),
track_map_point: StringArrayHashMap(MapPointData),
pub const LoadError = error{ pub const LoadError = error{
NotStarted, NotStarted,

View File

@@ -0,0 +1,10 @@
pub const file = "TrackMapPointTable.json";
start: []const u8,
end: []const u8,
quest: []const u8,
pos: struct {
x: f32,
y: f32,
z: f32,
},

View File

@@ -3,6 +3,8 @@ const json = std.json;
pub const CommonSkillConfig = @import("configs/CommonSkillConfig.zig"); pub const CommonSkillConfig = @import("configs/CommonSkillConfig.zig");
pub const LevelConfig = @import("configs/LevelConfig.zig"); pub const LevelConfig = @import("configs/LevelConfig.zig");
pub const ClientSingleMapMarkData = @import("configs/ClientSingleMapMarkData.zig");
pub const TeleportValidationDataTable = @import("configs/TeleportValidationDataTable.zig");
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;

View File

@@ -0,0 +1,35 @@
const ClientSingleMapMarkData = @This();
basicData: MapMarkBasicData,
detailedData: ?MapMarkDetailedData = null,
pub const MapMarkBasicData = struct {
templateId: []const u8,
markInstId: []const u8,
pos: struct {
x: f32,
y: f32,
z: f32,
},
};
pub const MapMarkDetailedData = struct {
logicIdGlobal: ?u64 = null,
teleportValidationId: ?[]const u8 = null,
pub const TeleportValidationData = struct {
logicIdGlobal: u64,
teleportValidationId: []const u8,
};
};
pub fn teleportValidationData(
csmmd: *const ClientSingleMapMarkData,
) ?MapMarkDetailedData.TeleportValidationData {
const details = csmmd.detailedData orelse return null;
return .{
.logicIdGlobal = details.logicIdGlobal orelse return null,
.teleportValidationId = details.teleportValidationId orelse return null,
};
}

View File

@@ -0,0 +1,10 @@
const std = @import("std");
teleportValidationDatas: std.json.ArrayHashMap(TeleportValidationData),
pub const TeleportValidationData = struct {
id: []const u8,
teleportReason: i32,
sceneId: []const u8,
position: struct { x: f32, y: f32, z: f32 },
};

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";
@@ -19,6 +20,9 @@ const char_bag_teams_file = "teams";
const char_bag_meta_file = "meta"; const char_bag_meta_file = "meta";
const item_bag_path = "item_bag"; const item_bag_path = "item_bag";
const item_bag_weapon_depot_file = "weapon_depot"; const item_bag_weapon_depot_file = "weapon_depot";
const scene_path = "scene";
const scene_current_file = "current";
const default_level = "map02_lv001";
const default_team: []const []const u8 = &.{ const default_team: []const []const u8 = &.{
"chr_0026_lastrite", "chr_0026_lastrite",
@@ -35,10 +39,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 +59,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);
@@ -82,26 +88,31 @@ pub fn loadPlayer(io: Io, gpa: Allocator, assets: *const Assets, uid: u64) !Play
else => |e| return e, else => |e| return e,
}; };
result.scene = loadSceneComponent(io, data_dir, uid) catch |err| switch (err) {
error.NeedsReset => try createDefaultSceneComponent(io, data_dir, assets),
else => |e| return e,
};
return result; return result;
} }
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 +127,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 +157,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 +175,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 +206,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 +409,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},
); );
} }
@@ -443,12 +454,81 @@ fn createDefaultBitsetComponent(io: Io, data_dir: Io.Dir, assets: *const Assets)
return bitset; return bitset;
} }
fn loadSceneComponent(io: Io, data_dir: Io.Dir, uid: []const u8) !Player.Scene {
const scene_dir = data_dir.openDir(io, scene_path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.NeedsReset,
error.Canceled => |e| return e,
else => return error.InputOutput,
};
defer scene_dir.close(io);
const current = fs.loadStruct(Player.Scene.Current, io, scene_dir, scene_current_file) catch |err| switch (err) {
inline error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => |e| {
if (e == error.ChecksumMismatch) {
log.err(
"checksum mismatched for current scene data of player {s}, resetting to defaults.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for current scene data of player {s}, resetting to defaults.",
.{uid},
);
}
return error.NeedsReset;
},
error.Canceled => |e| return e,
else => return error.InputOutput,
};
return .{ .current = current };
}
fn createDefaultSceneComponent(io: Io, data_dir: Io.Dir, assets: *const Assets) !Player.Scene {
const scene_dir = data_dir.createDirPathOpen(io, scene_path, .{}) catch |err| switch (err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
};
const default_level_config = assets.level_config_table.getPtr(default_level).?;
const current: Player.Scene.Current = .{
.level_id = default_level_config.idNum,
.position = .{
default_level_config.playerInitPos.x,
default_level_config.playerInitPos.y,
default_level_config.playerInitPos.z,
},
.rotation = .{
default_level_config.playerInitRot.x,
default_level_config.playerInitRot.y,
default_level_config.playerInitRot.z,
},
};
try fs.saveStruct(Player.Scene.Current, &current, io, scene_dir, scene_current_file);
return .{ .current = current };
}
pub fn saveSceneComponent(io: Io, data_dir: Io.Dir, component: *const Player.Scene) !void {
const scene_dir = data_dir.createDirPathOpen(io, scene_path, .{}) catch |err| switch (err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
};
try fs.saveStruct(Player.Scene.Current, &component.current, io, scene_dir, scene_current_file);
}
fn loadArray( fn loadArray(
comptime T: type, comptime T: type,
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 +536,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

@@ -10,6 +10,7 @@ pub const Unlock = @import("Player/Unlock.zig");
pub const CharBag = @import("Player/CharBag.zig"); pub const CharBag = @import("Player/CharBag.zig");
pub const ItemBag = @import("Player/ItemBag.zig"); pub const ItemBag = @import("Player/ItemBag.zig");
pub const Bitset = @import("Player/Bitset.zig"); pub const Bitset = @import("Player/Bitset.zig");
pub const Scene = @import("Player/Scene.zig");
base: Base, base: Base,
game_vars: GameVars, game_vars: GameVars,
@@ -17,6 +18,7 @@ unlock: Unlock,
char_bag: CharBag, char_bag: CharBag,
item_bag: ItemBag, item_bag: ItemBag,
bitset: Bitset, bitset: Bitset,
scene: Scene,
pub fn deinit(player: *Player, gpa: Allocator) void { pub fn deinit(player: *Player, gpa: Allocator) void {
player.game_vars.deinit(gpa); player.game_vars.deinit(gpa);

View File

@@ -0,0 +1,9 @@
const Scene = @This();
current: Current,
pub const Current = struct {
level_id: i32,
position: [3]f32,
rotation: [3]f32,
};

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

@@ -1,5 +1,9 @@
pub const Login = struct {}; pub const Login = struct {};
pub const ChangeSceneBegin = struct {};
pub const CurrentSceneModified = struct {};
pub const CharBagTeamModified = struct { pub const CharBagTeamModified = struct {
team_index: usize, team_index: usize,
modification: enum { modification: enum {

View File

@@ -15,6 +15,7 @@ const namespaces = &.{
@import("messaging/scene.zig"), @import("messaging/scene.zig"),
@import("messaging/char_bag.zig"), @import("messaging/char_bag.zig"),
@import("messaging/friend_chat.zig"), @import("messaging/friend_chat.zig"),
@import("messaging/map_mark.zig"),
}; };
pub fn Request(comptime CSType: type) type { pub fn Request(comptime CSType: type) type {

View File

@@ -0,0 +1,56 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Assets = @import("../../Assets.zig");
const Player = logic.Player;
const messaging = logic.messaging;
pub fn onSceneSetTrackPoint(
request: messaging.Request(pb.CS_SCENE_SET_TRACK_POINT),
assets: *const Assets,
scene: Player.Component(.scene),
change_scene_tx: logic.event.Sender(.change_scene_begin),
cur_scene_modified_tx: logic.event.Sender(.current_scene_modified),
) !void {
const log = std.log.scoped(.scene_set_track_point);
const track_point = request.message.track_point orelse return;
const point_config = assets.map_mark_table.get(track_point.inst_id) orelse {
log.debug("invalid point instance id: '{s}'", .{track_point.inst_id});
return;
};
const teleport_validation = point_config.teleportValidationData() orelse {
// Not a teleport point.
return;
};
const validation_config = assets.teleport_validation_table.teleportValidationDatas.map.getPtr(teleport_validation.teleportValidationId) orelse {
log.debug(
"teleport validation config '{s}' doesn't exist",
.{teleport_validation.teleportValidationId},
);
return;
};
const level_config = assets.level_config_table.getPtr(validation_config.sceneId) orelse {
log.debug("level with id '{s}' doesn't exist", .{validation_config.sceneId});
return;
};
scene.data.current.level_id = level_config.idNum;
scene.data.current.position = .{
validation_config.position.x,
validation_config.position.y,
validation_config.position.z,
};
try cur_scene_modified_tx.send(.{});
try change_scene_tx.send(.{});
log.info(
"transitioning to scene '{s}', position: {any}",
.{ validation_config.sceneId, scene.data.current.position },
);
}

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;
}, },
@@ -36,3 +36,31 @@ pub fn saveCharBagTeams(
}, },
}; };
} }
pub fn saveCurrentScene(
_: logic.event.Receiver(.current_scene_modified),
scene: Player.Component(.scene),
player_id: logic.World.PlayerId,
io: Io,
) !void {
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 {s}: {t}",
.{ player_id.uid.view(), e },
);
return;
},
};
defer data_dir.close(io);
fs.persistence.saveSceneComponent(io, data_dir, scene.data) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("save failed: {t}", .{e});
return;
},
};
}

View File

@@ -8,32 +8,37 @@ const Player = logic.Player;
const ArrayList = std.ArrayList; const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const default_level = "map02_lv001";
pub fn enterSceneOnLogin( pub fn enterSceneOnLogin(
rx: logic.event.Receiver(.login), rx: logic.event.Receiver(.login),
tx: logic.event.Sender(.change_scene_begin),
) !void {
_ = rx;
try tx.send(.{});
}
pub fn beginChangingScene(
rx: logic.event.Receiver(.change_scene_begin),
session: *Session, session: *Session,
assets: *const Assets, base: Player.Component(.base),
base_comp: Player.Component(.base), scene: Player.Component(.scene),
) !void { ) !void {
_ = rx; _ = rx;
const level_config = assets.level_config_table.getPtr(default_level).?;
const position: pb.VECTOR = .{ const position: pb.VECTOR = .{
.X = level_config.playerInitPos.x, .X = scene.data.current.position[0],
.Y = level_config.playerInitPos.y, .Y = scene.data.current.position[1],
.Z = level_config.playerInitPos.z, .Z = scene.data.current.position[2],
}; };
try session.send(pb.SC_CHANGE_SCENE_BEGIN_NOTIFY{ try session.send(pb.SC_CHANGE_SCENE_BEGIN_NOTIFY{
.scene_num_id = level_config.idNum, .scene_num_id = scene.data.current.level_id,
.position = position, .position = position,
.pass_through_data = .init, .pass_through_data = .init,
}); });
try session.send(pb.SC_ENTER_SCENE_NOTIFY{ try session.send(pb.SC_ENTER_SCENE_NOTIFY{
.role_id = base_comp.data.role_id, .role_id = base.data.role_id,
.scene_num_id = level_config.idNum, .scene_num_id = scene.data.current.level_id,
.position = position, .position = position,
.pass_through_data = .init, .pass_through_data = .init,
}); });
@@ -57,7 +62,8 @@ pub fn syncSelfScene(
rx: logic.event.Receiver(.sync_self_scene), rx: logic.event.Receiver(.sync_self_scene),
session: *Session, session: *Session,
arena: logic.Resource.Allocator(.arena), arena: logic.Resource.Allocator(.arena),
char_bag: logic.Player.Component(.char_bag), char_bag: Player.Component(.char_bag),
scene: Player.Component(.scene),
assets: *const Assets, assets: *const Assets,
) !void { ) !void {
const reason: pb.SELF_INFO_REASON_TYPE = switch (rx.payload.reason) { const reason: pb.SELF_INFO_REASON_TYPE = switch (rx.payload.reason) {
@@ -65,18 +71,17 @@ pub fn syncSelfScene(
.team_modified => .SLR_CHANGE_TEAM, .team_modified => .SLR_CHANGE_TEAM,
}; };
const level_config = assets.level_config_table.getPtr(default_level).?;
const position: pb.VECTOR = .{ const position: pb.VECTOR = .{
.X = level_config.playerInitPos.x, .X = scene.data.current.position[0],
.Y = level_config.playerInitPos.y, .Y = scene.data.current.position[1],
.Z = level_config.playerInitPos.z, .Z = scene.data.current.position[2],
}; };
const team_index = char_bag.data.meta.curr_team_index; const team_index = char_bag.data.meta.curr_team_index;
const leader_index = char_bag.data.teams.items(.leader_index)[team_index]; const leader_index = char_bag.data.teams.items(.leader_index)[team_index];
var self_scene_info: pb.SC_SELF_SCENE_INFO = .{ var self_scene_info: pb.SC_SELF_SCENE_INFO = .{
.scene_num_id = level_config.idNum, .scene_num_id = scene.data.current.level_id,
.self_info_reason = @intFromEnum(reason), .self_info_reason = @intFromEnum(reason),
.teamInfo = .{ .teamInfo = .{
.team_type = .CHAR_BAG_TEAM_TYPE_MAIN, .team_type = .CHAR_BAG_TEAM_TYPE_MAIN,
@@ -106,7 +111,7 @@ pub fn syncSelfScene(
.templateid = char_template_id, .templateid = char_template_id,
.position = position, .position = position,
.rotation = .{}, .rotation = .{},
.scene_num_id = level_config.idNum, .scene_num_id = scene.data.current.level_id,
}, },
}; };