Release 0.1.0

This commit is contained in:
xeon
2026-02-02 20:53:22 +03:00
commit 25660300dd
152 changed files with 882089 additions and 0 deletions

112
gamesv/src/Assets.zig Normal file
View File

@@ -0,0 +1,112 @@
const Assets = @This();
const std = @import("std");
pub const configs = @import("Assets/configs.zig");
pub const Tables = @import("Assets/Tables.zig");
pub const CharacterSkillMap = @import("Assets/CharacterSkillMap.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const meta = std.meta;
const log = std.log.scoped(.assets);
arena: ArenaAllocator,
owned_tables: Tables.Owned,
char_skill_map: CharacterSkillMap,
str_to_num_dicts: IndexDictionaries.StrToNum,
num_to_str_dicts: IndexDictionaries.NumToStr,
common_skill_config: configs.CommonSkillConfig,
level_config_table: std.StringArrayHashMapUnmanaged(configs.LevelConfig),
pub const IdGroup = enum {
char_id,
item_id,
};
const IndexDictionaries = blk: {
const names = meta.fieldNames(IdGroup);
break :blk .{
.StrToNum = @Struct(.auto, null, names, &@splat(*const Tables.StrToNum), &@splat(.{})),
.NumToStr = @Struct(.auto, null, names, &@splat(*const Tables.NumToStr), &@splat(.{})),
};
};
pub fn load(io: Io, gpa: Allocator) !Assets {
const owned_tables = try Tables.load(io, gpa);
errdefer owned_tables.deinit();
var arena: ArenaAllocator = .init(gpa);
errdefer arena.deinit();
const char_skill_map = try CharacterSkillMap.init(arena.allocator(), &owned_tables.tables);
var str_to_num_dicts: IndexDictionaries.StrToNum = undefined;
var num_to_str_dicts: IndexDictionaries.NumToStr = undefined;
inline for (@typeInfo(IdGroup).@"enum".fields) |field| {
@field(str_to_num_dicts, field.name) = owned_tables.tables.str_to_num.getPtr(field.name) orelse {
log.err("missing str-to-num dictionary: " ++ field.name, .{});
return error.MissingData;
};
@field(num_to_str_dicts, field.name) = owned_tables.tables.num_to_str.getPtr(field.name) orelse {
log.err("missing num-to-str dictionary: " ++ field.name, .{});
return error.MissingData;
};
}
const common_skill_config = try configs.loadJsonConfig(
configs.CommonSkillConfig,
io,
arena.allocator(),
configs.CommonSkillConfig.file,
);
const level_config_table = try configs.loadJsonConfig(
std.json.ArrayHashMap(configs.LevelConfig),
io,
arena.allocator(),
"LevelConfigTable.json",
);
return .{
.arena = arena,
.owned_tables = owned_tables,
.char_skill_map = char_skill_map,
.str_to_num_dicts = str_to_num_dicts,
.num_to_str_dicts = num_to_str_dicts,
.common_skill_config = common_skill_config,
.level_config_table = level_config_table.map,
};
}
pub fn deinit(assets: *Assets) void {
assets.owned_tables.deinit();
assets.arena.deinit();
}
pub inline fn table(
assets: *const Assets,
comptime t: std.meta.FieldEnum(Tables),
) *const @FieldType(Tables, @tagName(t)) {
return &@field(assets.owned_tables.tables, @tagName(t));
}
pub fn strToNum(
assets: *const Assets,
comptime group: IdGroup,
str: []const u8,
) ?i32 {
const str_to_num = @field(assets.str_to_num_dicts, @tagName(group));
return str_to_num.dic.map.get(str);
}
pub fn numToStr(
assets: *const Assets,
comptime group: IdGroup,
num: i32,
) ?[]const u8 {
const num_to_str = @field(assets.num_to_str_dicts, @tagName(group));
return num_to_str.dic.map.get(num);
}

View File

@@ -0,0 +1,79 @@
// Maps character ids to list of their skills.
const CharacterSkillMap = @This();
const std = @import("std");
const Tables = @import("Tables.zig");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.skill_map);
map: std.StringArrayHashMapUnmanaged(CharacterSkills),
const CharacterSkills = struct {
all_skills: []const []const u8,
combo_skill: []const u8,
normal_skill: []const u8,
attack_skill: []const u8,
ultimate_skill: []const u8,
};
pub fn init(arena: Allocator, tables: *const Tables) !CharacterSkillMap {
var result: CharacterSkillMap = .{ .map = .empty };
for (tables.character.keys()) |char_id| {
var skill_ids: std.ArrayList([]const u8) = .empty;
var combo_skill: ?[]const u8 = null;
var normal_skill: ?[]const u8 = null;
var attack_skill: ?[]const u8 = null;
var ultimate_skill: ?[]const u8 = null;
for (tables.skill_patch.keys()) |skill_id| {
if (std.mem.startsWith(u8, skill_id, char_id)) {
try skill_ids.append(arena, skill_id);
if (std.mem.find(u8, skill_id, "normal_skill") != null) {
normal_skill = skill_id;
} else if (std.mem.find(u8, skill_id, "combo_skill") != null) {
combo_skill = skill_id;
} else if (std.mem.find(u8, skill_id, "ultimate_skill") != null) {
ultimate_skill = skill_id;
} else if (std.mem.find(u8, skill_id, "_attack1") != null) {
attack_skill = skill_id;
}
}
}
if (skill_ids.items.len == 0) // Dummy Character
continue;
if (combo_skill == null) {
log.err("no combo_skill for {s}", .{char_id});
return error.MalformedData;
}
if (normal_skill == null) {
log.err("no normal_skill for {s}", .{char_id});
return error.MalformedData;
}
if (attack_skill == null) {
log.err("no attack_skill for {s}", .{char_id});
return error.MalformedData;
}
if (ultimate_skill == null) {
log.err("no ultimate_skill for {s}", .{char_id});
return error.MalformedData;
}
try result.map.put(arena, char_id, .{
.combo_skill = combo_skill.?,
.normal_skill = normal_skill.?,
.attack_skill = attack_skill.?,
.ultimate_skill = ultimate_skill.?,
.all_skills = skill_ids.items,
});
}
return result;
}

View File

@@ -0,0 +1,223 @@
const Tables = @This();
const std = @import("std");
const json = std.json;
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const StringArrayHashMap = std.StringArrayHashMapUnmanaged;
const log = std.log.scoped(.tables);
pub const CharacterData = @import("Tables/CharacterData.zig");
pub const SkillPatchDataBundleList = @import("Tables/SkillPatchDataBundleList.zig");
pub const WeaponBasicData = @import("Tables/WeaponBasicData.zig");
pub const CharWpnRecommendData = @import("Tables/CharWpnRecommendData.zig");
pub const DomainData = @import("Tables/DomainData.zig");
pub const StrToNum = struct {
pub const file = "StrIdNumTable.json";
dic: StringTable(i32),
};
pub const NumToStr = struct {
pub const file = "NumIdStrTable.json";
dic: IntTable(i32, []const u8),
};
character: StringArrayHashMap(CharacterData),
skill_patch: StringArrayHashMap(SkillPatchDataBundleList),
weapon_basic: StringArrayHashMap(WeaponBasicData),
str_to_num: StringArrayHashMap(StrToNum),
num_to_str: StringArrayHashMap(NumToStr),
char_wpn_recommend: StringArrayHashMap(CharWpnRecommendData),
domain_data: StringArrayHashMap(DomainData),
pub const LoadError = error{
NotStarted,
ReadFail,
ParseFail,
} || Io.Cancelable || Allocator.Error;
const LoadResults = blk: {
var field_names: []const []const u8 = &.{};
for (@typeInfo(Tables).@"struct".fields) |field| {
field_names = field_names ++ .{field.name};
}
var field_types: [field_names.len]type = undefined;
var field_attrs: [field_names.len]std.builtin.Type.StructField.Attributes = undefined;
for (field_names, 0..) |name, i| {
field_types[i] = LoadError!@FieldType(Tables, name);
field_attrs[i] = .{
.default_value_ptr = &@as(LoadError!@FieldType(Tables, name), LoadError.NotStarted),
};
}
break :blk @Struct(.auto, null, field_names, &field_types, &field_attrs);
};
pub const Owned = struct {
tables: Tables,
arenas: [@typeInfo(Tables).@"struct".fields.len]?ArenaAllocator,
pub fn deinit(owned: Owned) void {
for (owned.arenas) |maybe_arena| if (maybe_arena) |arena| {
arena.deinit();
};
}
};
pub fn load(io: Io, gpa: Allocator) (error{LoadFailed} || Io.Cancelable)!Owned {
var owned: Owned = .{
.tables = undefined,
.arenas = @splat(null),
};
errdefer owned.deinit();
var loaders: Io.Group = .init;
defer loaders.cancel(io);
var results: LoadResults = .{};
inline for (@typeInfo(Tables).@"struct".fields, 0..) |field, i| {
owned.arenas[i] = .init(gpa);
loaders.async(
io,
Loader(field.type).startLoading,
.{ &@field(results, field.name), io, owned.arenas[i].?.allocator() },
);
}
try loaders.await(io);
var has_errors = false;
inline for (@typeInfo(Tables).@"struct".fields) |field| {
if (@field(results, field.name)) |table| {
@field(owned.tables, field.name) = table;
} else |err| switch (err) {
error.Canceled => return error.Canceled,
else => |e| {
has_errors = true;
log.err("failed to load table '{s}': {t}", .{ field.name, e });
},
}
}
return if (!has_errors) owned else error.LoadFailed;
}
fn Loader(comptime Table: type) type {
return struct {
pub fn startLoading(
result: *LoadError!Table,
io: Io,
arena: Allocator,
) Io.Cancelable!void {
const Value = @FieldType(Table.KV, "value");
const file = Io.Dir.cwd().openFile(io, "assets/tables/" ++ Value.file, .{}) catch |err| switch (err) {
error.Canceled => return error.Canceled,
else => {
result.* = LoadError.ReadFail;
return;
},
};
defer file.close(io);
var buffer: [16384]u8 = undefined;
var file_reader = file.reader(io, &buffer);
var json_reader: json.Reader = .init(arena, &file_reader.interface);
defer json_reader.deinit();
if (json.parseFromTokenSourceLeaky(
StringTable(Value),
arena,
&json_reader,
.{ .ignore_unknown_fields = true },
)) |st| {
result.* = st.map;
} else |_| {
result.* = LoadError.ParseFail;
}
}
};
}
// HashMap wrapper to deserialize from an array of ["Key": "String", "Value": {...}]
fn StringTable(comptime V: type) type {
return struct {
const ST = @This();
map: StringArrayHashMap(V) = .empty,
const IntermediateKV = struct {
Key: []const u8,
Value: V,
};
pub fn jsonParse(a: std.mem.Allocator, source: anytype, options: json.ParseOptions) !ST {
if (try source.nextAlloc(a, options.allocate.?) != .array_begin)
return error.UnexpectedToken;
var map: StringArrayHashMap(V) = .empty;
errdefer map.deinit(a);
while (source.peekNextTokenType()) |t| switch (t) {
.object_begin => {
const kv = json.innerParse(IntermediateKV, a, source, options) catch unreachable;
try map.put(a, kv.Key, kv.Value);
},
.array_end => {
_ = try source.next();
break;
},
else => return error.UnexpectedToken,
} else |err| return err;
return .{ .map = map };
}
};
}
// HashMap wrapper to deserialize from an array of ["Key": Int, "Value": {...}]
fn IntTable(comptime K: type, comptime V: type) type {
return struct {
const ST = @This();
map: std.AutoArrayHashMapUnmanaged(K, V) = .empty,
const IntermediateKV = struct {
Key: K,
Value: V,
};
pub fn jsonParse(a: std.mem.Allocator, source: anytype, options: json.ParseOptions) !ST {
if (try source.nextAlloc(a, options.allocate.?) != .array_begin)
return error.UnexpectedToken;
var map: std.AutoArrayHashMapUnmanaged(K, V) = .empty;
errdefer map.deinit(a);
while (source.peekNextTokenType()) |t| switch (t) {
.object_begin => {
const kv = json.innerParse(IntermediateKV, a, source, options) catch unreachable;
try map.put(a, kv.Key, kv.Value);
},
.array_end => {
_ = try source.next();
break;
},
else => return error.UnexpectedToken,
} else |err| return err;
return .{ .map = map };
}
};
}

View File

@@ -0,0 +1,6 @@
pub const file = "CharWpnRecommendTable.json";
charId: []const u8,
weaponIds1: []const []const u8,
weaponIds2: []const []const u8,
weaponIds3: []const []const u8,

View File

@@ -0,0 +1,120 @@
pub const file = "CharacterTable.json";
pub const AttributeDataPack = struct {
Attribute: AttributeData,
breakStage: i32,
};
pub const AttributeData = struct {
attrs: []const AttributePair,
};
pub const AttributePair = struct {
attrType: AttributeType,
attrValue: f64,
};
pub const AttributeType = enum(i32) {
level = 0,
max_hp = 1,
atk = 2,
def = 3,
physical_damage_taken_scalar = 4,
fire_damage_taken_scalar = 5,
pulse_damage_taken_scalar = 6,
cryst_damage_taken_scalar = 7,
weight = 8,
critical_rate = 9,
critical_damage_increase = 10,
hatred = 11,
normal_attack_range = 12,
move_speed_scalar = 13,
turn_rate_scalar = 14,
attack_rate = 15,
skill_cooldown_scalar = 16,
normal_attack_damage_increase = 17,
hp_recovery_per_sec = 18,
hp_recovery_per_sec_by_max_hp_ratio = 19,
max_poise = 20,
poise_rec_time = 21,
max_ultimate_sp = 22,
damage_taken_scalar_with_poise = 23,
poise_damage_taken_scalar = 24,
physical_infliction_damage_scalar = 25,
poise_damage_output_scalar = 26,
breaking_attack_damage_taken_scalar = 27,
ultimate_skill_damage_increase = 28,
heal_output_increase = 29,
heal_taken_increase = 30,
poise_rec_time_scalar = 31,
normal_skill_damage_increase = 32,
combo_skill_damage_increase = 33,
knock_down_time_addition = 34,
fire_burst_damage_increase = 35,
pulse_burst_damage_increase = 36,
cryst_burst_damage_increase = 37,
natural_burst_damage_increase = 38,
str = 39,
agi = 40,
wisd = 41,
will = 42,
life_steal = 43,
ultimate_sp_gain_scalar = 44,
atb_cost_addition = 45,
skill_cooldown_addition = 46,
combo_skill_cooldown_scalar = 47,
natural_damage_taken_scalar = 48,
ignite_damage_scalar = 49,
physical_damage_increase = 50,
fire_damage_increase = 51,
pulse_damage_increase = 52,
cryst_damage_increase = 53,
natural_damage_increase = 54,
ether_damage_increase = 55,
fire_abnormal_damage_increase = 56,
pulse_abnormal_damage_increase = 57,
cryst_abnormal_damage_increase = 58,
natural_abnormal_damage_increase = 59,
ether_damage_taken_scalar = 60,
damage_to_broken_unit_increase = 61,
weakness_dmg_scalar = 62,
shelter_dmg_scalar = 63,
physical_enhanced_dmg_increase = 64,
fire_enhanced_dmg_increase = 65,
pulse_enhanced_dmg_increase = 66,
cryst_enhanced_dmg_increase = 67,
natural_enhanced_dmg_increase = 68,
ether_enhanced_dmg_increase = 69,
physical_vulnerable_dmg_increase = 70,
fire_vulnerable_dmg_increase = 71,
pulse_vulnerable_dmg_increase = 72,
cryst_vulnerable_dmg_increase = 73,
natural_vulnerable_dmg_increase = 74,
ether_vulnerable_dmg_increase = 75,
atk_increase_factor_from_str = 76,
atk_increase_factor_from_agi = 77,
atk_increase_factor_from_wisd = 78,
atk_increase_factor_from_will = 79,
physical_dmg_resist_scalar = 80,
natural_dmg_resist_scalar = 81,
cryst_dmg_resist_scalar = 82,
pulse_dmg_resist_scalar = 83,
fire_dmg_resist_scalar = 84,
ether_dmg_resist_scalar = 85,
slow_action_speed_scalar = 86,
physical_and_spell_infliction_enhance = 87,
shield_output_increase = 88,
shield_taken_increase = 89,
};
attributes: []const AttributeDataPack,
charBattleTagIds: []const []const u8,
charId: []const u8,
mainAttrType: i32,
profession: u32,
rarity: u32,
resilienceDeductionFactor: f32,
sortOrder: u32,
subAttrType: i32,
superArmor: u32,
weaponType: u32,

View File

@@ -0,0 +1,4 @@
pub const file = "DomainDataTable.json";
domainId: []const u8,
levelGroup: []const []const u8,

View File

@@ -0,0 +1,12 @@
pub const file = "SkillPatchTable.json";
SkillPatchDataBundle: []const SkillPatchData,
pub const SkillPatchData = struct {
coolDown: f32,
costType: u32,
costValue: f32,
level: u32,
maxChargeTime: u32,
skillId: []const u8,
};

View File

@@ -0,0 +1,8 @@
pub const file = "WeaponBasicTable.json";
weaponId: []const u8,
weaponPotentialSkill: []const u8,
weaponSkillList: []const []const u8,
maxLv: u32,
rarity: u32,
weaponType: u32,

View File

@@ -0,0 +1,34 @@
const std = @import("std");
const json = std.json;
pub const CommonSkillConfig = @import("configs/CommonSkillConfig.zig");
pub const LevelConfig = @import("configs/LevelConfig.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
pub fn loadJsonConfig(
comptime T: type,
io: Io,
arena: Allocator,
filename: []const u8,
) !T {
const config_dir = try Io.Dir.cwd().openDir(io, "assets/configs/", .{});
defer config_dir.close(io);
const file = try config_dir.openFile(io, filename, .{});
defer file.close(io);
var buffer: [16384]u8 = undefined;
var file_reader = file.reader(io, &buffer);
var json_reader: json.Reader = .init(arena, &file_reader.interface);
defer json_reader.deinit();
return try json.parseFromTokenSourceLeaky(
T,
arena,
&json_reader,
.{ .ignore_unknown_fields = true },
);
}

View File

@@ -0,0 +1,14 @@
pub const file = "CommonSkillConfig.json";
config: struct {
Character: SkillConfigList,
},
pub const SkillConfigList = struct {
skillConfigs: []const SkillConfig,
};
pub const SkillConfig = struct {
skillId: []const u8,
skillType: u32,
};

View File

@@ -0,0 +1,16 @@
id: []const u8,
idNum: i32,
scope: u8,
isSeamless: bool,
mapIdStr: []const u8,
isDimensionLevel: bool,
dimensionSourceLevelId: []const u8,
startPos: Vector,
playerInitPos: Vector,
playerInitRot: Vector,
pub const Vector = struct {
x: f32,
y: f32,
z: f32,
};

217
gamesv/src/Session.zig Normal file
View File

@@ -0,0 +1,217 @@
const Session = @This();
const std = @import("std");
const proto = @import("proto");
const logic = @import("logic.zig");
const network = @import("network.zig");
const auth = @import("Session/auth.zig");
const fs = @import("fs.zig");
const Assets = @import("Assets.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Crc32 = std.hash.Crc32;
const pb = proto.pb;
const net = Io.net;
const first_request_timeout: Io.Duration = .fromSeconds(5);
const subsequent_request_timeout: Io.Duration = .fromSeconds(30);
pub const ConcurrencyAvailability = enum {
undetermined,
unavailable,
available,
};
pub const IoOptions = struct {
// Indicates whether Io.concurrent() should be considered.
concurrency: ConcurrencyAvailability,
// Specifies the preferred system clock.
preferred_clock: Io.Clock,
};
writer: *Io.Writer,
client_seq_id: u64 = 0,
server_seq_id: u64 = 0,
pub fn process(
io: Io,
gpa: Allocator,
assets: *const Assets,
stream: net.Stream,
options: IoOptions,
) Io.Cancelable!void {
const log = std.log.scoped(.net);
defer stream.close(io);
log.debug("new connection from '{f}'", .{stream.socket.address});
defer log.debug("client from '{f}' disconnected", .{stream.socket.address});
var recv_buffer: [64 * 1024]u8 = undefined;
var send_buffer: [4 * 1024]u8 = undefined;
var reader = stream.reader(io, &recv_buffer);
var writer = stream.writer(io, &send_buffer);
var session: Session = .{
.writer = &writer.interface,
};
var world: ?logic.World = null;
defer if (world) |*w| w.deinit(gpa);
var receive_timeout = first_request_timeout;
while (receiveNetRequest(io, &reader.interface, receive_timeout, options)) |request| {
session.client_seq_id = request.head.up_seqid;
log.debug("received header: {any}", .{request.head});
log.debug("received body: {X}", .{request.body});
if (world) |*w| {
logic.messaging.process(gpa, w, &request) catch |err| switch (err) {
error.MissingHandler => log.warn("no handler for {t}", .{request.msgId()}),
error.DecodeFailed => {
log.err(
"received malformed message of type '{t}' from '{f}', disconnecting",
.{ request.msgId(), stream.socket.address },
);
return;
},
error.Canceled, error.WriteFailed, error.OutOfMemory => return,
};
} else {
const result = processFirstRequest(io, gpa, &session, &request) catch |err| switch (err) {
error.UnexpectedMessage => {
log.err(
"received unexpected first message '{t}' from '{f}', disconnecting",
.{ request.msgId(), stream.socket.address },
);
return;
},
error.DecodeFailed => {
log.err(
"received malformed login request from '{t}', disconnecting",
.{stream.socket.address},
);
return;
},
error.LoginFailed => {
log.err(
"session from '{f}' has failed to login, disconnecting",
.{stream.socket.address},
);
return;
},
// Regardless which one, the session is invalidated by now.
error.WriteFailed, error.OutOfMemory => return,
error.Canceled => |e| return e,
};
const player = fs.persistence.loadPlayer(io, gpa, assets, result.uid) 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 });
return;
},
};
log.info(
"client from '{f}' has successfully logged into account with uid: {d}",
.{ stream.socket.address, result.uid },
);
world = logic.World.init(&session, assets, result.uid, player, gpa, io);
receive_timeout = subsequent_request_timeout;
logic.systems.triggerEvent(.{ .login = .{} }, &world.?, gpa) catch |err| switch (err) {
error.Canceled, error.OutOfMemory, error.WriteFailed => return,
};
}
} else |err| switch (err) {
error.Canceled,
error.ConcurrencyUnavailable,
error.ReadFailed,
error.EndOfStream,
=> {},
error.HeadDecodeError,
error.ChecksumMismatch,
error.InvalidMessageId,
=> |e| log.err(
"failed to receive request from '{f}': {t}",
.{ stream.socket.address, e },
),
}
}
pub const SendError = Io.Writer.Error;
pub fn send(session: *Session, message: anytype) SendError!void {
var buffer: [128]u8 = undefined;
var discarding: Io.Writer.Discarding = .init("");
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&discarding.writer, .init(), &buffer);
proto.encodeMessage(&hashed.writer, message) catch unreachable; // Discarding + Hashed can't fail.
hashed.writer.flush() catch unreachable;
const head: pb.CSHead = .{
.msgid = @intFromEnum(proto.messageId(@TypeOf(message))),
.up_seqid = session.client_seq_id, // Why? No idea. But FlushSync kills itself if it's not like that
.down_seqid = 0,
.total_pack_count = 0,
.checksum = hashed.hasher.final(),
};
const head_size = proto.encodingLength(head);
const body_size = discarding.fullCount();
try session.writer.writeInt(u8, @intCast(head_size), .little);
try session.writer.writeInt(u16, @intCast(body_size), .little);
try proto.encodeMessage(session.writer, head);
try proto.encodeMessage(session.writer, message);
try session.writer.flush();
session.server_seq_id += 1;
}
fn processFirstRequest(io: Io, gpa: Allocator, session: *Session, request: *const network.Request) !auth.Result {
if (request.msgId() != .cs_login)
return error.UnexpectedMessage;
var reader: Io.Reader = .fixed(request.body);
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
const cs_login = proto.decodeMessage(&reader, arena.allocator(), pb.CS_LOGIN) catch
return error.DecodeFailed;
return try auth.processLoginRequest(io, session, &cs_login);
}
const ReceiveError = Io.Cancelable || Io.ConcurrentError || network.Request.ReadError;
fn receiveNetRequest(
io: Io,
reader: *Io.Reader,
timeout: Io.Duration,
options: IoOptions,
) ReceiveError!network.Request {
return switch (options.concurrency) {
.undetermined => unreachable,
.unavailable => try network.Request.read(reader),
.available => {
var receive = try io.concurrent(network.Request.read, .{reader});
errdefer _ = receive.cancel(io) catch {};
var sleep = try io.concurrent(Io.sleep, .{ io, timeout, options.preferred_clock });
defer sleep.cancel(io) catch {};
return switch (try io.select(.{
.receive = &receive,
.sleep = &sleep,
})) {
.sleep => try receive.cancel(io),
.receive => |request| request,
};
},
};
}

View File

@@ -0,0 +1,29 @@
const std = @import("std");
const pb = @import("proto").pb;
const Session = @import("../Session.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
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
};
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
return error.LoginFailed;
try session.send(pb.SC_LOGIN{
.uid = request.uid,
.server_time = @intCast((Io.Clock.real.now(io) catch Io.Timestamp.zero).toSeconds()),
.server_time_zone = 3,
});
return .{ .uid = uid };
}

344
gamesv/src/fs.zig Normal file
View File

@@ -0,0 +1,344 @@
const std = @import("std");
pub const persistence = @import("fs/persistence.zig");
const Io = std.Io;
const Dir = Io.Dir;
const File = Io.File;
const Allocator = std.mem.Allocator;
const Crc32 = std.hash.Crc32;
const MultiArrayList = std.MultiArrayList;
const log = std.log.scoped(.fs);
pub const RepresentationError = error{ReprSizeMismatch};
pub const LoadStructError = error{
SystemResources,
FileNotFound,
InputOutput,
ChecksumMismatch,
} || RepresentationError || Io.Cancelable;
pub const LoadDynamicArrayError = Allocator.Error || LoadStructError;
pub const SaveStructError = error{
SystemResources,
InputOutput,
} || Io.Cancelable;
const struct_header_size: usize = checksum_size;
const checksum_size: usize = 4;
const ArrayHeader = struct {
checksum: u32,
item_count: u32,
};
pub fn loadStruct(comptime T: type, io: Io, dir: Dir, sub_path: []const u8) LoadStructError!T {
const repr_size = @sizeOf(T);
var result: T = undefined;
const file = dir.openFile(io, sub_path, .{}) catch |err| switch (err) {
error.FileNotFound, error.SystemResources, error.Canceled => |e| return e,
else => |e| {
log.debug("fs.loadStruct('{s}') openFile failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
defer file.close(io);
const length = file.length(io) catch |err| switch (err) {
error.Streaming => unreachable,
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("fs.loadStruct('{s}') File.length() failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
if (length != repr_size + struct_header_size)
return RepresentationError.ReprSizeMismatch;
var file_reader = file.reader(io, "");
const reader = &file_reader.interface;
var checksum: [4]u8 = undefined;
reader.readSliceAll(&checksum) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
const bytes: [*]u8 = @ptrCast(&result);
var bytes_writer: Io.Writer = .fixed(bytes[0..repr_size]);
var writer_buf: [128]u8 = undefined; // Just to amortize vtable calls.
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&bytes_writer, .init(), &writer_buf);
reader.streamExact(&hashed.writer, repr_size) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
hashed.writer.flush() catch unreachable;
if (hashed.hasher.final() != std.mem.readInt(u32, &checksum, .native))
return error.ChecksumMismatch;
return result;
}
pub fn saveStruct(comptime T: type, data: *const T, io: Io, dir: Dir, sub_path: []const u8) !void {
const repr_size = @sizeOf(T);
const file = dir.createFile(io, sub_path, .{}) catch |err| switch (err) {
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("saveStruct('{s}'): createFile failed: {t}", .{ sub_path, e });
return error.InputOutput;
},
};
defer file.close(io);
var file_writer_buf: [1024]u8 = undefined;
var file_writer = file.writer(io, &file_writer_buf);
// Checksum placeholder.
file_writer.interface.writeInt(u32, 0, .native) catch return error.InputOutput;
var hashed_writer_buf: [128]u8 = undefined; // Just to amortize vtable calls.
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&file_writer.interface, .init(), &hashed_writer_buf);
const bytes: [*]const u8 = @ptrCast(data);
hashed.writer.writeAll(bytes[0..repr_size]) catch return error.InputOutput;
hashed.writer.flush() catch return error.InputOutput;
file_writer.seekTo(0) catch return error.InputOutput;
file_writer.interface.writeInt(u32, hashed.hasher.final(), .native) catch return error.InputOutput;
file_writer.interface.flush() catch return error.InputOutput;
}
pub fn loadDynamicArray(
comptime Elem: type,
io: Io,
dir: Dir,
gpa: Allocator,
sub_path: []const u8,
) LoadDynamicArrayError![]Elem {
const elem_size = @sizeOf(Elem);
const file = dir.openFile(io, sub_path, .{}) catch |err| switch (err) {
error.FileNotFound, error.SystemResources, error.Canceled => |e| return e,
else => |e| {
log.debug("fs.loadDynamicArray('{s}') openFile failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
defer file.close(io);
const length = file.length(io) catch |err| switch (err) {
error.Streaming => unreachable,
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("fs.loadDynamicArray('{s}') File.length() failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
if (length < @sizeOf(ArrayHeader))
return RepresentationError.ReprSizeMismatch;
var file_reader = file.reader(io, "");
const reader = &file_reader.interface;
var header: ArrayHeader = undefined;
reader.readSliceAll(@ptrCast(&header)) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
if (length < (elem_size * header.item_count) + @sizeOf(ArrayHeader))
return RepresentationError.ReprSizeMismatch;
const result = try gpa.alloc(Elem, header.item_count);
errdefer gpa.free(result);
const bytes: [*]u8 = @ptrCast(result);
var bytes_writer: Io.Writer = .fixed(bytes[0 .. elem_size * header.item_count]);
var writer_buf: [128]u8 = undefined; // Just to amortize vtable calls.
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&bytes_writer, .init(), &writer_buf);
reader.streamExact(&hashed.writer, elem_size * header.item_count) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
hashed.writer.flush() catch unreachable;
if (hashed.hasher.final() != header.checksum)
return error.ChecksumMismatch;
return result;
}
pub fn saveDynamicArray(comptime Elem: type, array: []const Elem, io: Io, dir: Dir, sub_path: []const u8) SaveStructError!void {
std.debug.assert(array.len <= std.math.maxInt(u32));
const file = dir.createFile(io, sub_path, .{}) catch |err| switch (err) {
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("saveDynamicArray('{s}'): createFile failed: {t}", .{ sub_path, e });
return error.InputOutput;
},
};
defer file.close(io);
var file_writer_buf: [1024]u8 = undefined;
var file_writer = file.writer(io, &file_writer_buf);
// Checksum placeholder.
file_writer.interface.writeInt(u32, 0, .native) catch return error.InputOutput;
file_writer.interface.writeInt(u32, @truncate(array.len), .native) catch return error.InputOutput;
var hashed_writer_buf: [128]u8 = undefined; // Just to amortize vtable calls.
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&file_writer.interface, .init(), &hashed_writer_buf);
hashed.writer.writeAll(@ptrCast(array)) catch return error.InputOutput;
hashed.writer.flush() catch return error.InputOutput;
file_writer.seekTo(0) catch return error.InputOutput;
file_writer.interface.writeInt(u32, hashed.hasher.final(), .native) catch return error.InputOutput;
file_writer.interface.flush() catch return error.InputOutput;
}
pub fn loadMultiArrayList(
comptime Elem: type,
io: Io,
dir: Dir,
gpa: Allocator,
sub_path: []const u8,
) LoadDynamicArrayError!MultiArrayList(Elem) {
const file = dir.openFile(io, sub_path, .{}) catch |err| switch (err) {
error.FileNotFound, error.SystemResources, error.Canceled => |e| return e,
else => |e| {
log.debug("fs.loadMultiArrayList('{s}') openFile failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
defer file.close(io);
const length = file.length(io) catch |err| switch (err) {
error.Streaming => unreachable,
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("fs.loadMultiArrayList('{s}') File.length() failed: {t}", .{ sub_path, e });
return error.SystemResources;
},
};
if (length < @sizeOf(ArrayHeader))
return RepresentationError.ReprSizeMismatch;
var file_reader = file.reader(io, "");
var header: ArrayHeader = undefined;
file_reader.interface.readSliceAll(@ptrCast(&header)) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
const bytes_length = MultiArrayList(Elem).capacityInBytes(header.item_count);
if (length < bytes_length + @sizeOf(ArrayHeader))
return RepresentationError.ReprSizeMismatch;
var result = try MultiArrayList(Elem).initCapacity(gpa, header.item_count);
errdefer result.deinit(gpa);
result.len = header.item_count;
const fields = comptime std.enums.values(MultiArrayList(Elem).Field);
var vecs: [fields.len][]u8 = undefined;
var slice = result.slice();
inline for (fields) |field| {
vecs[@intFromEnum(field)] = std.mem.sliceAsBytes(slice.items(field));
}
var hashed = file_reader.interface.hashed(Crc32.init(), "");
hashed.reader.readVecAll(&vecs) catch |err| switch (err) {
error.ReadFailed => switch (file_reader.err.?) {
error.Canceled, error.SystemResources => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
if (hashed.hasher.final() != header.checksum)
return error.ChecksumMismatch;
return result;
}
pub fn saveMultiArrayList(
comptime Elem: type,
list: *const MultiArrayList(Elem),
io: Io,
dir: Dir,
sub_path: []const u8,
) SaveStructError!void {
std.debug.assert(list.len <= std.math.maxInt(u32));
const file = dir.createFile(io, sub_path, .{}) catch |err| switch (err) {
error.Canceled, error.SystemResources => |e| return e,
else => |e| {
log.debug("saveMultiArrayList('{s}'): createFile failed: {t}", .{ sub_path, e });
return error.InputOutput;
},
};
defer file.close(io);
var file_writer_buf: [1024]u8 = undefined;
var file_writer = file.writer(io, &file_writer_buf);
// Checksum placeholder.
file_writer.interface.writeInt(u32, 0, .native) catch return error.InputOutput;
file_writer.interface.writeInt(u32, @truncate(list.len), .native) catch return error.InputOutput;
var hashed_writer_buf: [128]u8 = undefined; // Just to amortize vtable calls.
var hashed: Io.Writer.Hashed(Crc32) = .initHasher(&file_writer.interface, .init(), &hashed_writer_buf);
const fields = comptime std.enums.values(MultiArrayList(Elem).Field);
var vecs: [fields.len][]const u8 = undefined;
var slice = list.slice();
inline for (fields) |field| {
vecs[@intFromEnum(field)] = std.mem.sliceAsBytes(slice.items(field));
}
hashed.writer.writeVecAll(&vecs) catch return error.InputOutput;
hashed.writer.flush() catch return error.InputOutput;
file_writer.seekTo(0) catch return error.InputOutput;
file_writer.interface.writeInt(u32, hashed.hasher.final(), .native) catch return error.InputOutput;
file_writer.interface.flush() catch return error.InputOutput;
}

View File

@@ -0,0 +1,477 @@
const std = @import("std");
const fs = @import("../fs.zig");
const logic = @import("../logic.zig");
const Assets = @import("../Assets.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const Player = logic.Player;
const player_data_dir = "store/player/";
const base_component_file = "base_data";
const server_game_vars_file = "server_game_vars";
const client_game_vars_file = "client_game_vars";
const unlocked_systems_file = "unlocked_systems";
const bitset_file = "bitset";
const char_bag_path = "char_bag";
const char_bag_chars_file = "chars";
const char_bag_teams_file = "teams";
const char_bag_meta_file = "meta";
const item_bag_path = "item_bag";
const item_bag_weapon_depot_file = "weapon_depot";
const default_team: []const []const u8 = &.{
"chr_0026_lastrite",
"chr_0009_azrila",
"chr_0016_laevat",
"chr_0022_bounda",
};
const LoadPlayerError = error{
InputOutput,
SystemResources,
} || Allocator.Error || Io.Cancelable;
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.
const cwd: Io.Dir = .cwd();
return cwd.openDir(io, dir_path, .{}) catch |open_err| switch (open_err) {
error.Canceled => |e| return e,
error.FileNotFound => cwd.createDirPathOpen(io, dir_path, .{}) catch |create_err| switch (create_err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
},
else => return error.InputOutput,
};
}
// 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 {
const data_dir = try openPlayerDataDir(io, uid);
defer data_dir.close(io);
var result: Player = undefined;
result.base = try loadBaseComponent(io, data_dir, uid);
result.game_vars = try loadGameVarsComponent(io, gpa, data_dir, uid);
errdefer result.game_vars.deinit(gpa);
result.unlock = try loadUnlockComponent(io, gpa, data_dir, uid);
errdefer result.unlock.deinit(gpa);
result.item_bag = try loadItemBagComponent(io, gpa, data_dir);
errdefer result.item_bag.deinit(gpa);
result.char_bag = loadCharBagComponent(io, gpa, data_dir, uid) catch |err| switch (err) {
error.NeedsReset => try createDefaultCharBagComponent(io, gpa, assets, &result.item_bag, data_dir),
else => |e| return e,
};
errdefer result.char_bag.deinit(gpa);
result.bitset = loadBitsetComponent(io, data_dir, uid) catch |err| switch (err) {
error.NeedsReset => try createDefaultBitsetComponent(io, data_dir, assets),
else => |e| return e,
};
return result;
}
fn loadBaseComponent(
io: Io,
data_dir: Io.Dir,
uid: u64,
) !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.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for base_data of player {d}, resetting to defaults.",
.{uid},
);
}
var defaults: Player.Base = .init;
try fs.saveStruct(Player.Base, &defaults, io, data_dir, base_component_file);
break :reset defaults;
},
error.Canceled => |e| return e,
else => return error.InputOutput,
};
}
fn loadGameVarsComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Player.GameVars {
var game_vars: Player.GameVars = undefined;
game_vars.server_vars = try loadArray(
Player.GameVars.ServerVar,
io,
gpa,
data_dir,
uid,
server_game_vars_file,
Player.GameVars.default_server_vars,
);
errdefer gpa.free(game_vars.server_vars);
game_vars.client_vars = try loadArray(
Player.GameVars.ClientVar,
io,
gpa,
data_dir,
uid,
client_game_vars_file,
Player.GameVars.default_client_vars,
);
errdefer gpa.free(game_vars.client_vars);
return game_vars;
}
fn loadUnlockComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !Player.Unlock {
var unlock: Player.Unlock = undefined;
unlock.unlocked_systems = try loadArray(
Player.Unlock.SystemType,
io,
gpa,
data_dir,
uid,
unlocked_systems_file,
Player.Unlock.default_unlocked_systems,
);
errdefer gpa.free(unlock.unlocked_systems);
return unlock;
}
fn loadCharBagComponent(io: Io, gpa: Allocator, data_dir: Io.Dir, uid: u64) !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,
else => return error.InputOutput,
};
defer char_bag_dir.close(io);
var chars = fs.loadMultiArrayList(Player.CharBag.Char, io, char_bag_dir, gpa, char_bag_chars_file) catch |err| switch (err) {
error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => return error.NeedsReset,
error.Canceled, error.OutOfMemory => |e| return e,
else => return error.InputOutput,
};
errdefer chars.deinit(gpa);
var teams = fs.loadMultiArrayList(Player.CharBag.Team, io, char_bag_dir, gpa, char_bag_teams_file) catch |err| switch (err) {
error.FileNotFound, error.ChecksumMismatch, error.ReprSizeMismatch => return error.NeedsReset,
error.Canceled, error.OutOfMemory => |e| return e,
else => return error.InputOutput,
};
errdefer teams.deinit(gpa);
if (teams.len == 0) return error.NeedsReset;
const meta = fs.loadStruct(Player.CharBag.Meta, io, char_bag_dir, char_bag_meta_file) catch |err| switch (err) {
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.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for char bag metadata of player {d}, resetting to defaults.",
.{uid},
);
}
const defaults: Player.CharBag.Meta = .{ .curr_team_index = 0 };
try fs.saveStruct(Player.CharBag.Meta, &defaults, io, data_dir, base_component_file);
break :reset defaults;
},
error.Canceled => |e| return e,
else => return error.InputOutput,
};
if (meta.curr_team_index >= teams.len)
return error.NeedsReset;
return .{
.chars = chars,
.teams = teams,
.meta = meta,
};
}
fn createDefaultCharBagComponent(
io: Io,
gpa: Allocator,
assets: *const Assets,
// Depends on ItemBag because it has to create weapons for the new characters.
item_bag: *Player.ItemBag,
data_dir: Io.Dir,
) !Player.CharBag {
const char_bag_dir = data_dir.createDirPathOpen(io, char_bag_path, .{}) catch |err| switch (err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
};
defer char_bag_dir.close(io);
var chars = try std.MultiArrayList(
Player.CharBag.Char,
).initCapacity(gpa, assets.table(.character).count());
errdefer chars.deinit(gpa);
for (assets.table(.character).keys(), assets.table(.character).values()) |id, char_data| {
const char_id_num = assets.strToNum(.char_id, id) orelse continue;
if (!assets.char_skill_map.map.contains(id))
continue; // Dummy Character
const weapon_template_id: i32 = blk: {
if (assets.table(.char_wpn_recommend).getPtr(id)) |recommend| {
if (recommend.weaponIds1.len > 0)
break :blk assets.strToNum(.item_id, recommend.weaponIds1[0]).?;
}
for (assets.table(.weapon_basic).values()) |weapon| {
if (weapon.weaponType == char_data.weaponType)
break :blk assets.strToNum(.item_id, weapon.weaponId).?;
} else continue; // No suitable weapon, don't create this character because it'll be broken in-game.
};
try item_bag.weapon_depot.append(gpa, .{
.template_id = weapon_template_id,
.exp = 0,
.weapon_lv = 1,
.refine_lv = 0,
.breakthrough_lv = 0,
.attach_gem_id = 0,
});
const weapon_id: Player.ItemBag.WeaponIndex = @enumFromInt(
@as(u64, @intCast(item_bag.weapon_depot.len - 1)),
);
const hp = for (char_data.attributes[0].Attribute.attrs) |attr| {
if (attr.attrType == .max_hp)
break attr.attrValue;
} else 100;
const sp = for (char_data.attributes[0].Attribute.attrs) |attr| {
if (attr.attrType == .max_ultimate_sp)
break attr.attrValue;
} else 100;
chars.appendAssumeCapacity(.{
.template_id = char_id_num,
.level = 1,
.exp = 0,
.is_dead = false,
.hp = hp,
.ultimate_sp = @floatCast(sp),
.weapon_id = weapon_id,
.own_time = 0,
.equip_medicine_id = 0,
.potential_level = 5,
});
}
var teams = try std.MultiArrayList(Player.CharBag.Team).initCapacity(gpa, 1);
errdefer teams.deinit(gpa);
var result: Player.CharBag = .{
.chars = chars,
.teams = teams,
.meta = .{ .curr_team_index = 0 },
};
var team: Player.CharBag.Team.SlotArray = @splat(.empty);
for (default_team, 0..) |char_template_id, i| {
const id_num = assets.strToNum(.char_id, char_template_id).?;
const char_index = result.charIndexById(id_num).?;
team[i] = .fromCharIndex(char_index);
}
result.teams.appendAssumeCapacity(.{
.name = .constant("reversedrooms"),
.char_team = team,
.leader_index = team[0].charIndex().?,
});
try saveItemBagComponent(io, data_dir, item_bag);
try fs.saveMultiArrayList(Player.CharBag.Char, &result.chars, io, char_bag_dir, char_bag_chars_file);
try fs.saveMultiArrayList(Player.CharBag.Team, &result.teams, io, char_bag_dir, char_bag_teams_file);
try fs.saveStruct(Player.CharBag.Meta, &result.meta, io, char_bag_dir, char_bag_meta_file);
return result;
}
fn loadItemBagComponent(io: Io, gpa: Allocator, data_dir: Io.Dir) !Player.ItemBag {
const item_bag_dir = data_dir.openDir(io, "item_bag", .{}) catch |open_err| switch (open_err) {
error.FileNotFound => data_dir.createDirPathOpen(io, "item_bag", .{}) catch |create_err| switch (create_err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
},
error.Canceled => |e| return e,
else => return error.InputOutput,
};
defer item_bag_dir.close(io);
var weapon_depot = fs.loadMultiArrayList(Player.ItemBag.Weapon, io, item_bag_dir, gpa, item_bag_weapon_depot_file) catch |err| switch (err) {
error.FileNotFound,
error.ChecksumMismatch,
error.ReprSizeMismatch,
=> std.MultiArrayList(Player.ItemBag.Weapon).empty,
error.Canceled, error.OutOfMemory => |e| return e,
else => return error.InputOutput,
};
errdefer weapon_depot.deinit(gpa);
return .{ .weapon_depot = weapon_depot };
}
pub fn saveItemBagComponent(io: Io, data_dir: Io.Dir, component: *const Player.ItemBag) !void {
const item_bag_dir = data_dir.createDirPathOpen(io, item_bag_path, .{}) catch |err| switch (err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
};
defer item_bag_dir.close(io);
try fs.saveMultiArrayList(Player.ItemBag.Weapon, &component.weapon_depot, io, item_bag_dir, item_bag_weapon_depot_file);
}
pub fn saveCharBagComponent(
io: Io,
data_dir: Io.Dir,
component: *const Player.CharBag,
comptime what: union(enum) { all, chars, teams, meta },
) !void {
const char_bag_dir = data_dir.createDirPathOpen(io, char_bag_path, .{}) catch |err| switch (err) {
error.Canceled => |e| return e,
else => return error.InputOutput,
};
defer char_bag_dir.close(io);
if (what == .all or what == .chars) {
try fs.saveMultiArrayList(Player.CharBag.Char, &component.chars, io, char_bag_dir, char_bag_chars_file);
}
if (what == .all or what == .teams) {
try fs.saveMultiArrayList(Player.CharBag.Team, &component.teams, io, char_bag_dir, char_bag_teams_file);
}
if (what == .all or what == .meta) {
try fs.saveStruct(Player.CharBag.Meta, &component.meta, io, char_bag_dir, char_bag_meta_file);
}
}
fn loadBitsetComponent(io: Io, data_dir: Io.Dir, uid: u64) !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.",
.{uid},
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for bitset of player {d}, resetting to defaults.",
.{uid},
);
}
return error.NeedsReset;
},
error.Canceled => |e| return e,
else => return error.InputOutput,
};
}
fn createDefaultBitsetComponent(io: Io, data_dir: Io.Dir, assets: *const Assets) !Player.Bitset {
var bitset: Player.Bitset = .init;
for (assets.level_config_table.values()) |config| {
bitset.set(.level_have_been, @intCast(config.idNum)) catch |err| switch (err) {
error.ValueOutOfRange => { // This means we have to increase Bitset.max_value
std.debug.panic(
"createDefaultBitsetComponent: value is out of range! ({d}/{d})",
.{ config.idNum, Player.Bitset.max_value },
);
},
};
bitset.set(.level_map_first_view, @intCast(config.idNum)) catch unreachable;
bitset.set(.read_level, @intCast(config.idNum)) catch unreachable;
}
try fs.saveStruct(Player.Bitset, &bitset, io, data_dir, bitset_file);
return bitset;
}
fn loadArray(
comptime T: type,
io: Io,
gpa: Allocator,
data_dir: Io.Dir,
uid: u64,
sub_path: []const u8,
defaults: []const T,
) ![]T {
return fs.loadDynamicArray(T, io, data_dir, gpa, sub_path) catch |err| switch (err) {
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.",
.{ sub_path, uid },
);
}
if (e == error.ReprSizeMismatch) {
log.err(
"struct layout mismatched for '{s}' of player {d}, resetting to defaults.",
.{ sub_path, uid },
);
}
try fs.saveDynamicArray(T, defaults, io, data_dir, sub_path);
break :reset try gpa.dupe(T, defaults);
},
error.Canceled, error.OutOfMemory => |e| return e,
else => return error.InputOutput,
};
}

7
gamesv/src/logic.zig Normal file
View File

@@ -0,0 +1,7 @@
pub const World = @import("logic/World.zig");
pub const Resource = @import("logic/Resource.zig");
pub const Player = @import("logic/Player.zig");
pub const messaging = @import("logic/messaging.zig");
pub const event = @import("logic/event.zig");
pub const systems = @import("logic/systems.zig");
pub const queries = @import("logic/queries.zig");

View File

@@ -0,0 +1,45 @@
const Player = @This();
const std = @import("std");
const meta = std.meta;
const Allocator = std.mem.Allocator;
pub const Base = @import("Player/Base.zig");
pub const GameVars = @import("Player/GameVars.zig");
pub const Unlock = @import("Player/Unlock.zig");
pub const CharBag = @import("Player/CharBag.zig");
pub const ItemBag = @import("Player/ItemBag.zig");
pub const Bitset = @import("Player/Bitset.zig");
base: Base,
game_vars: GameVars,
unlock: Unlock,
char_bag: CharBag,
item_bag: ItemBag,
bitset: Bitset,
pub fn deinit(player: *Player, gpa: Allocator) void {
player.game_vars.deinit(gpa);
player.unlock.deinit(gpa);
player.char_bag.deinit(gpa);
player.item_bag.deinit(gpa);
}
// Describes the dependency on an individual player component.
pub fn Component(comptime tag: meta.FieldEnum(Player)) type {
return struct {
pub const player_component_tag = tag;
data: *@FieldType(Player, @tagName(tag)),
};
}
pub fn isComponent(comptime T: type) bool {
if (!@hasDecl(T, "player_component_tag")) return false;
return T == Component(T.player_component_tag);
}
pub fn getComponentByType(player: *Player, comptime T: type) T {
return .{ .data = &@field(player, @tagName(T.player_component_tag)) };
}

View File

@@ -0,0 +1,37 @@
const Base = @This();
const common = @import("common");
const mem = common.mem;
pub const max_role_name_length: usize = 15;
pub const init: Base = .{
.create_ts = 0,
.role_name = .constant("xeondev"),
.role_id = 1,
.level = .first,
.exp = 0,
.create_ts_display = 0,
.gender = .default,
};
pub const Gender = enum(u8) {
pub const default: Gender = .male;
invalid = 0,
male = 1,
female = 2,
};
pub const Level = enum(u8) {
first = 1,
last = 60,
_,
};
create_ts: i64,
role_name: mem.LimitedString(max_role_name_length),
role_id: u64,
level: Level,
exp: u32,
create_ts_display: i64,
gender: Gender,

View File

@@ -0,0 +1,113 @@
const Bitset = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const max_value = 512;
const Set = std.bit_set.ArrayBitSet(u64, max_value);
pub const init: Bitset = .{};
sets: [Type.count]Set = @splat(.initEmpty()),
pub fn set(b: *Bitset, t: Type, value: u64) error{ValueOutOfRange}!void {
if (value > max_value) return error.ValueOutOfRange;
b.sets[@intFromEnum(t) - 1].set(@intCast(value));
}
pub const Type = enum(u32) {
pub const count: usize = blk: {
const values = std.enums.values(Type);
break :blk @as(usize, @intFromEnum(values[values.len - 1])) + 1;
};
found_item = 1,
wiki = 2,
unread_wiki = 3,
monster_drop = 4,
got_item = 5,
area_first_view = 6,
unread_got_item = 7,
prts = 8,
unread_prts = 9,
prts_first_lv = 10,
prts_terminal_content = 11,
level_have_been = 12,
level_map_first_view = 13,
unread_formula = 14,
new_char = 15,
elog_channel = 16,
fmv_watched = 17,
time_line_watched = 18,
map_filter = 19,
friend_has_request = 20,
equip_tech_formula = 21,
radio_trigger = 22,
remote_communication_finish = 23,
unlock_server_dungeon_series = 24,
chapter_first_view = 25,
adventure_level_reward_done = 26,
dungeon_entrance_touched = 27,
equip_tech_tier = 28,
char_doc = 30,
char_voice = 31,
reading_pop = 32,
reward_id_done = 33,
prts_investigate = 34,
racing_received_bp_node = 35,
racing_complete_achievement = 36,
racing_received_achievement = 37,
interactive_active = 39,
mine_point_first_time_collect = 40,
unread_char_doc = 41,
unread_char_voice = 42,
area_toast_once = 44,
unread_equip_tech_formula = 45,
prts_investigate_unread_note = 46,
prts_investigate_note = 47,
game_mechanic_read = 48,
read_active_blackbox = 49,
read_level = 50,
factroy_placed_building = 51,
interactive_two_state = 52,
unread_unlock_spaceship_room_type = 53,
unlock_spaceship_room_type = 54,
unlock_user_avatar = 55,
unlock_user_avatar_frame = 56,
unlock_business_card_topic = 57,
special_game_event = 58,
radio_id = 59,
got_weapon = 60,
read_new_version_equip_tech_formula = 61,
mist_map_unlocked = 62,
read_achive = 63,
camera_volume = 64,
read_fac_tech_tree_unhidden_tech = 65,
read_fac_tech_tree_unhidden_category = 66,
mist_map_mv_watched = 67,
remote_communication_wait_for_play = 68,
mission_completed_once = 69,
psn_cup_unlocked = 70,
unread_week_raid_mission = 71,
unlock_game_entrance_activity_series = 72,
unlock_domain_depot = 73,
unlock_recycle_bin = 74,
manual_crafted_item = 75,
un_read_new_activity_notify = 76,
read_picture_ids = 77,
read_shop_id = 78,
read_shop_goods_id = 79,
read_bp_season_id = 80,
read_bp_task_id = 81,
read_cash_shop_goods_id = 82,
new_avatar_unlock = 83,
new_avatar_frame_unlock = 84,
new_theme_unlock = 85,
read_char_potential_pic_ids = 86,
read_high_difficulty_dungeon_series = 87,
reported_client_log_types = 88,
activated_factory_inst = 89,
read_max_world_level = 90,
got_formula_unlock_item = 91,
};

View File

@@ -0,0 +1,102 @@
const CharBag = @This();
const std = @import("std");
const common = @import("common");
const Player = @import("../Player.zig");
const Allocator = std.mem.Allocator;
teams: std.MultiArrayList(Team),
chars: std.MultiArrayList(Char),
meta: Meta,
pub const CharIndex = enum(u64) {
_,
// Returns an 'objectId' for network serialization.
pub fn objectId(i: CharIndex) u64 {
return @intFromEnum(i) + 1;
}
pub fn fromObjectId(id: u64) CharIndex {
return @enumFromInt(id - 1);
}
};
pub fn deinit(bag: *CharBag, gpa: Allocator) void {
bag.teams.deinit(gpa);
bag.chars.deinit(gpa);
}
pub fn charIndexById(bag: *const CharBag, template_id: i32) ?CharIndex {
const idx: u64 = @intCast(
std.mem.findScalar(i32, bag.chars.items(.template_id), template_id) orelse
return null,
);
return @enumFromInt(idx);
}
pub fn charIndexWithWeapon(bag: *const CharBag, weapon: Player.ItemBag.WeaponIndex) ?CharIndex {
const idx: u64 = @intCast(
std.mem.findScalar(Player.ItemBag.WeaponIndex, bag.chars.items(.weapon_id), weapon) orelse
return null,
);
return @enumFromInt(idx);
}
// Checks:
// 1. Existence of the team.
// 2. Existence of the specified character index in the team.
pub fn ensureTeamMember(bag: *const CharBag, team_index: usize, char_index: CharIndex) error{
InvalidTeamIndex,
NotTeamMember,
}!void {
if (team_index < 0 or team_index >= bag.teams.len) {
return error.InvalidTeamIndex;
}
const char_team = &bag.teams.items(.char_team)[team_index];
_ = std.mem.findScalar(u64, @ptrCast(char_team), @intFromEnum(char_index)) orelse
return error.NotTeamMember;
}
pub const Meta = struct {
curr_team_index: u32,
};
pub const Team = struct {
pub const slots_count: usize = 4;
pub const SlotArray = [Team.slots_count]Team.Slot;
pub const Slot = enum(u64) {
empty = std.math.maxInt(u64),
_,
pub fn charIndex(s: Slot) ?CharIndex {
return if (s != .empty) @enumFromInt(@intFromEnum(s)) else null;
}
pub fn fromCharIndex(i: CharIndex) Slot {
return @enumFromInt(@intFromEnum(i));
}
};
name: common.mem.LimitedString(15) = .empty,
char_team: [slots_count]Slot = @splat(Slot.empty),
leader_index: CharIndex,
};
pub const Char = struct {
template_id: i32,
level: i32,
exp: i32,
is_dead: bool,
hp: f64,
ultimate_sp: f32,
weapon_id: Player.ItemBag.WeaponIndex,
own_time: i64,
equip_medicine_id: i32,
potential_level: u32,
};

View File

@@ -0,0 +1,71 @@
const GameVars = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const default_server_vars: []const ServerVar = &.{
.{ .key = .already_set_gender, .value = 1 },
.{ .key = .dash_energy_limit, .value = 100 },
.{ .key = .already_set_name, .value = 1 },
};
pub const default_client_vars: []const ClientVar = &.{
.{ .key = 43, .value = 1 },
.{ .key = 78, .value = 1 },
.{ .key = 82, .value = 1 },
.{ .key = 125, .value = 1 },
.{ .key = 126, .value = 1 },
};
pub const ClientVar = packed struct {
key: i32,
value: i64,
};
pub const ServerVar = packed struct {
key: ServerVarType,
value: i64,
};
client_vars: []ClientVar,
server_vars: []ServerVar,
pub fn deinit(gv: *GameVars, gpa: Allocator) void {
gpa.free(gv.client_vars);
gpa.free(gv.server_vars);
}
pub const ServerVarType = enum(i32) {
const common_begin: i32 = 100000;
const common_end: i32 = 109999;
const daily_refresh_begin: i32 = 110000;
const daily_refresh_end: i32 = 119999;
pub const Kind = enum(i32) {
common = 10,
daily_refresh = 11,
weekly_refresh = 12,
monthly_refresh = 13,
};
server_test_1 = 100001,
server_test_2 = 100002,
already_set_gender = 100003,
enhance_bean = 100004,
enhance_bean_last_replenish_time = 100005,
dash_energy_limit = 100006,
already_set_name = 100007,
social_share_control = 100008,
db_config_version = 100009,
client_debug_mode_end_time = 100010,
recover_ap_by_money_count = 110001,
poop_cow_interact_count = 110002,
stamina_reduce_used_count = 110003,
space_ship_daily_credit_reward = 110004,
daily_enemy_drop_mod_reward_count = 110005,
daily_enemy_exp_count = 110006,
pub inline fn kind(vt: ServerVarType) Kind {
return @enumFromInt(@intFromEnum(vt) / 10_000);
}
};

View File

@@ -0,0 +1,28 @@
const ItemBag = @This();
const std = @import("std");
const common = @import("common");
const Allocator = std.mem.Allocator;
weapon_depot: std.MultiArrayList(Weapon),
pub fn deinit(bag: *ItemBag, gpa: Allocator) void {
bag.weapon_depot.deinit(gpa);
}
pub const WeaponIndex = enum(u64) {
_,
pub fn instId(i: WeaponIndex) u64 {
return @intFromEnum(i) + 1;
}
};
pub const Weapon = struct {
template_id: i32,
exp: u32,
weapon_lv: u32,
refine_lv: u32,
breakthrough_lv: u32,
attach_gem_id: u64,
};

View File

@@ -0,0 +1,126 @@
const Unlock = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const default_unlocked_systems: []const SystemType = blk: {
const fields = @typeInfo(SystemType).@"enum".fields;
var list: []const SystemType = &.{};
for (fields) |field| {
if (field.value != @intFromEnum(SystemType.none)) {
list = list ++ .{@field(SystemType, field.name)};
}
}
break :blk list;
};
unlocked_systems: []SystemType,
pub fn deinit(unlock: *Unlock, gpa: Allocator) void {
gpa.free(unlock.unlocked_systems);
}
pub const SystemType = enum(i32) {
none = 10000000,
map = 0,
inventory = 1,
watch = 2,
valuable_depot = 3,
shop = 4,
char_team = 5,
gacha = 51,
dungeon = 52,
bloc_mission = 53,
mail = 54,
wiki = 55,
prts = 56,
submit_ether = 57,
scan = 58,
char_ui = 59,
friend = 60,
daily_mission = 61,
general_ability_bomb = 62,
general_ability_fluid_interact = 63,
general_ability = 64,
sns = 65,
equip_tech = 66,
equip_produce = 67,
dungeon_factory = 69,
enemy_spawner = 70,
general_ability_water_gun = 71,
general_ability_snapshot = 72,
fac_building_pin = 101,
fac_craft_pin = 102,
fac_mode = 103,
fac_tech_tree = 104,
fac_overview = 105,
fac_yield_stats = 106,
fac_conveyor = 107,
fac_transfer_port = 108,
fac_bridge = 109,
fac_splitter = 110,
fac_merger = 111,
fac_bus = 112,
fac_zone = 113,
fac_system = 114,
fac_pipe = 115,
fac_pipe_splitter = 116,
fac_pipe_connector = 117,
fac_pipe_converger = 118,
fac_hub = 119,
fac_bus_free = 120,
fac_top_view = 121,
fac_blueprint = 122,
fac_underground_pipe = 123,
fac_social = 124,
fac_valve = 125,
fac_pipe_valve = 126,
fac_panel_store = 127,
fac_fertilize = 128,
manual_craft = 201,
item_use = 202,
item_quick_bar = 203,
product_manual = 204,
manual_craft_soil = 205,
weapon = 251,
equip = 252,
equip_enhance = 253,
gem_enhance = 254,
normal_attack = 301,
normal_skill = 302,
ultimate_skill = 303,
team_skill = 304,
combo_skill = 305,
team_switch = 306,
dash = 307,
jump = 308,
lock_target = 309,
spaceship_present_gift = 401,
spaceship_manufacturing_station = 402,
spaceship_control_center = 403,
spaceship_system = 404,
spaceship_grow_cabin = 405,
spaceship_shop = 406,
spaceship_guest_room = 407,
settlement = 501,
domain_development = 502,
domain_development_domain_depot = 503,
settlement_defense = 504,
kite_station = 511,
domain_shop = 512,
racing_dungeon = 601,
battle_training = 602,
week_raid = 603,
week_raid_intro = 604,
water_drone_can_use_xiranite = 605,
adventure_exp_and_lv = 651,
adventure_book = 652,
guidance_manul = 661,
ai_bark = 670,
achievement = 701,
minigame_puzzle = 801,
bp_system = 802,
activity = 1100,
check_in = 1113,
};

View File

@@ -0,0 +1,46 @@
const Resource = @This();
const std = @import("std");
const mem = std.mem;
const Assets = @import("../Assets.zig");
const Io = std.Io;
pub const AllocatorKind = enum {
gpa,
arena,
};
pub const PingTimer = struct {
io: Io,
last_client_ts: u64 = 0,
pub fn serverTime(pt: PingTimer) u64 {
return if (Io.Clock.real.now(pt.io)) |ts|
@intCast(ts.toMilliseconds())
else |_|
pt.last_client_ts;
}
};
assets: *const Assets,
ping_timer: PingTimer,
pub fn init(assets: *const Assets, io_impl: Io) Resource {
return .{
.assets = assets,
.ping_timer = .{ .io = io_impl },
};
}
pub fn io(res: *const Resource) Io {
return res.ping_timer.io; // TODO: move to the root of resources.
}
// Describes the dependency on an allocator.
pub fn Allocator(comptime kind: AllocatorKind) type {
return struct {
pub const allocator_kind = kind;
interface: mem.Allocator,
};
}

View File

@@ -0,0 +1,58 @@
// Describes player-local state of the world.
const World = @This();
const std = @import("std");
const logic = @import("../logic.zig");
const Session = @import("../Session.zig");
const Assets = @import("../Assets.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
pub const PlayerId = struct { uid: u64 };
player_id: PlayerId,
session: *Session, // TODO: should it be here this way? Do we need an abstraction?
res: logic.Resource,
player: logic.Player,
pub fn init(
session: *Session,
assets: *const Assets,
uid: u64,
player: logic.Player,
gpa: Allocator,
io: Io,
) World {
_ = gpa;
return .{
.player_id = .{ .uid = uid },
.session = session,
.player = player,
.res = .init(assets, io),
};
}
pub fn deinit(world: *World, gpa: Allocator) void {
world.player.deinit(gpa);
}
pub const GetComponentError = error{
ComponentUnavailable,
};
pub fn getComponentByType(world: *World, comptime T: type) GetComponentError!T {
switch (T) {
PlayerId => return world.player_id,
*Session => return world.session,
*logic.Resource.PingTimer => return &world.res.ping_timer,
*const Assets => return world.res.assets,
Io => return world.res.io(),
else => {
if (comptime logic.Player.isComponent(T)) {
return world.player.getComponentByType(T);
}
@compileError("World.getComponentByType(" ++ @typeName(T) ++ ") is unsupported");
},
}
}

View File

@@ -0,0 +1,80 @@
const std = @import("std");
pub const kinds = @import("event/kinds.zig");
const Allocator = std.mem.Allocator;
const meta = std.meta;
// Describes the event receiver
pub fn Receiver(comptime kind: meta.Tag(Kind)) type {
return struct {
pub const rx_event_kind = kind;
pub const Event = @field(
kinds,
@typeInfo(kinds).@"struct".decls[@intFromEnum(kind)].name,
);
payload: Event,
};
}
// Describes the event sender
pub fn Sender(comptime kind: meta.Tag(Kind)) type {
return struct {
pub const tx_event_kind = kind;
pub const Event = @field(
kinds,
@typeInfo(kinds).@"struct".decls[@intFromEnum(kind)].name,
);
event_queue: *Queue,
pub fn send(s: @This(), event: Event) Allocator.Error!void {
try s.event_queue.push(@unionInit(Kind, @tagName(kind), event));
}
};
}
pub const Queue = struct {
arena: Allocator,
deque: std.Deque(Kind),
pub fn init(arena: Allocator) Queue {
return .{ .arena = arena, .deque = .empty };
}
pub fn push(queue: *Queue, event: Kind) Allocator.Error!void {
try queue.deque.pushBack(queue.arena, event);
}
};
pub const Kind = blk: {
var types: []const type = &.{};
var indices: []const u16 = &.{};
var names: []const []const u8 = &.{};
for (@typeInfo(kinds).@"struct".decls, 0..) |decl, i| {
const declaration = @field(kinds, decl.name);
if (@TypeOf(declaration) != type) continue;
if (meta.activeTag(@typeInfo(declaration)) == .@"struct") {
indices = indices ++ .{@as(u16, @intCast(i))};
types = types ++ .{@field(kinds, decl.name)};
names = names ++ .{toSnakeCase(decl.name)};
}
}
const EventTag = @Enum(u16, .exhaustive, names, indices[0..names.len]);
break :blk @Union(.auto, EventTag, names, types[0..names.len], &@splat(.{}));
};
inline fn toSnakeCase(comptime name: []const u8) []const u8 {
var result: []const u8 = "";
for (name, 0..) |c, i| {
if (std.ascii.isUpper(c)) {
if (i != 0) result = result ++ "_";
result = result ++ .{std.ascii.toLower(c)};
} else result = result ++ .{c};
}
return result;
}

View File

@@ -0,0 +1,16 @@
pub const Login = struct {};
pub const CharBagTeamModified = struct {
team_index: usize,
modification: enum {
set_leader,
set_char_team,
},
};
pub const SyncSelfScene = struct {
reason: enum {
entrance,
team_modified,
},
};

View File

@@ -0,0 +1,116 @@
const std = @import("std");
const proto = @import("proto");
const logic = @import("../logic.zig");
const network = @import("../network.zig");
const Session = @import("../Session.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.messaging);
const meta = std.meta;
const namespaces = &.{
@import("messaging/player.zig"),
@import("messaging/scene.zig"),
@import("messaging/char_bag.zig"),
@import("messaging/friend_chat.zig"),
};
pub fn Request(comptime CSType: type) type {
return struct {
pub const CSMessage = CSType;
message: *const CSMessage,
session: *Session,
};
}
const MsgID = blk: {
var msg_types: []const type = &.{};
for (namespaces) |namespace| {
for (@typeInfo(namespace).@"struct".decls) |decl_info| {
const decl = @field(namespace, decl_info.name);
const fn_info = switch (@typeInfo(@TypeOf(decl))) {
.@"fn" => |info| info,
else => continue,
};
if (fn_info.params.len == 0) continue;
const Param = fn_info.params[0].type.?;
if (!@hasDecl(Param, "CSMessage")) continue;
msg_types = msg_types ++ .{Param.CSMessage};
}
}
var msg_names: [msg_types.len][]const u8 = @splat("");
var msg_ids: [msg_types.len]i32 = @splat(0);
for (msg_types, 0..) |CSMsg, i| {
// Proven to exist by the code above.
msg_names[i] = CSMsg.message_name;
msg_ids[i] = @intFromEnum(proto.messageId(CSMsg));
}
break :blk @Enum(i32, .exhaustive, &msg_names, &msg_ids);
};
pub fn process(
gpa: Allocator,
world: *logic.World,
request: *const network.Request,
) !void {
const recv_msg_id = std.enums.fromInt(MsgID, request.head.msgid) orelse {
return error.MissingHandler;
};
switch (recv_msg_id) {
inline else => |msg_id| {
handler_lookup: inline for (namespaces) |namespace| {
inline for (@typeInfo(namespace).@"struct".decls) |decl_info| {
const decl = @field(namespace, decl_info.name);
const fn_info = switch (@typeInfo(@TypeOf(decl))) {
.@"fn" => |info| info,
else => continue,
};
if (fn_info.params.len == 0) continue;
const Param = fn_info.params[0].type.?;
if (!@hasDecl(Param, "CSMessage")) continue;
if (comptime !std.mem.eql(u8, @tagName(msg_id), Param.CSMessage.message_name))
continue;
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
var queue: logic.event.Queue = .init(arena.allocator());
var reader: Io.Reader = .fixed(request.body);
var message = proto.decodeMessage(&reader, arena.allocator(), Param.CSMessage) catch
return error.DecodeFailed;
var handler_args: meta.ArgsTuple(@TypeOf(decl)) = undefined;
handler_args[0] = .{
.message = &message,
.session = world.session,
};
inline for (fn_info.params[1..], 1..) |param, i| {
handler_args[i] = logic.queries.resolve(param.type.?, world, &queue, gpa, arena.allocator()) catch {
log.err("message handler for '{s}' requires an optional component", .{@typeName(Param.CSMessage)});
return;
};
}
try @call(.auto, decl, handler_args);
try logic.systems.run(world, &queue, gpa, arena.allocator());
break :handler_lookup;
}
} else comptime unreachable;
},
}
}

View File

@@ -0,0 +1,119 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Assets = @import("../../Assets.zig");
const event = logic.event;
const messaging = logic.messaging;
const Player = logic.Player;
pub fn onCharBagSetTeamLeader(
request: messaging.Request(pb.CS_CHAR_BAG_SET_TEAM_LEADER),
char_bag: Player.Component(.char_bag),
team_modified_tx: event.Sender(.char_bag_team_modified),
) !void {
const log = std.log.scoped(.char_bag_set_team_leader);
if ((request.message.team_type orelse .CHAR_BAG_TEAM_TYPE_MAIN) != .CHAR_BAG_TEAM_TYPE_MAIN)
return; // 'TEMP' teams are not supported.
const team_index = std.math.cast(usize, request.message.team_index) orelse {
log.err("invalid team index: {d}", .{request.message.team_index});
return;
};
const char_index: Player.CharBag.CharIndex = .fromObjectId(
request.message.leaderid,
);
char_bag.data.ensureTeamMember(team_index, char_index) catch |err| switch (err) {
error.InvalidTeamIndex => {
log.err(
"team index is out of range! {d}/{d}",
.{ team_index, char_bag.data.teams.len },
);
return;
},
error.NotTeamMember => {
log.err(
"character with index {d} is not a member of team {d}",
.{ @intFromEnum(char_index), team_index },
);
return;
},
};
const leader_index = &char_bag.data.teams.items(.leader_index)[team_index];
log.info(
"switching leader for team {d} ({d} -> {d})",
.{ team_index, leader_index.*, char_index },
);
leader_index.* = char_index;
try team_modified_tx.send(.{
.team_index = team_index,
.modification = .set_leader,
});
}
pub fn onCharBagSetTeam(
request: messaging.Request(pb.CS_CHAR_BAG_SET_TEAM),
char_bag: Player.Component(.char_bag),
team_modified_tx: event.Sender(.char_bag_team_modified),
) !void {
const log = std.log.scoped(.char_bag_set_team);
const team_index = std.math.cast(usize, request.message.team_index) orelse {
log.err("invalid team index: {d}", .{request.message.team_index});
return;
};
if (request.message.char_team.items.len > Player.CharBag.Team.slots_count) {
log.err(
"char_team exceeds slots count! {d}/{d}",
.{ request.message.char_team.items.len, Player.CharBag.Team.slots_count },
);
return;
}
if (std.mem.findScalar(u64, request.message.char_team.items, request.message.leader_id) == null) {
log.err("leader_id doesn't present in char_team", .{});
return;
}
var new_char_team: Player.CharBag.Team.SlotArray = @splat(.empty);
for (request.message.char_team.items, 0..) |char_id, i| {
if (std.mem.countScalar(u64, request.message.char_team.items, char_id) > 1) {
log.err("duplicated character id: {d}", .{char_id});
return;
}
const char_index: Player.CharBag.CharIndex = .fromObjectId(char_id);
if (@intFromEnum(char_index) >= char_bag.data.chars.len) {
log.err("invalid character object id: {d}", .{char_id});
return;
}
new_char_team[i] = .fromCharIndex(char_index);
}
const teams_slice = char_bag.data.teams.slice();
teams_slice.items(.char_team)[team_index] = new_char_team;
teams_slice.items(.leader_index)[team_index] = .fromObjectId(request.message.leader_id);
try team_modified_tx.send(.{
.team_index = team_index,
.modification = .set_char_team,
});
try request.session.send(pb.SC_CHAR_BAG_SET_TEAM{
.team_type = .CHAR_BAG_TEAM_TYPE_MAIN,
.team_index = request.message.team_index,
.char_team = request.message.char_team,
.scope_name = 1,
.leader_id = request.message.leader_id,
});
}

View File

@@ -0,0 +1,9 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const messaging = logic.messaging;
pub fn onCsFriendChatListSimpleSync(
request: messaging.Request(pb.CS_FRIEND_CHAT_LIST_SIMPLE_SYNC),
) !void {
try request.session.send(pb.SC_FRIEND_CHAT_LIST_SIMPLE_SYNC{});
}

View File

@@ -0,0 +1,27 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const messaging = logic.messaging;
pub fn onCsPing(
request: messaging.Request(pb.CS_PING),
timer: *logic.Resource.PingTimer,
) !void {
timer.last_client_ts = request.message.client_ts;
try request.session.send(pb.SC_PING{
.client_ts = request.message.client_ts,
.server_ts = timer.serverTime(),
});
}
pub fn onCsFlushSync(
request: messaging.Request(pb.CS_FLUSH_SYNC),
timer: *logic.Resource.PingTimer,
) !void {
timer.last_client_ts = request.message.client_ts;
try request.session.send(pb.SC_FLUSH_SYNC{
.client_ts = request.message.client_ts,
.server_ts = timer.serverTime(),
});
}

View File

@@ -0,0 +1,10 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const messaging = logic.messaging;
pub fn onSceneLoadFinish(
_: messaging.Request(pb.CS_SCENE_LOAD_FINISH),
sync_self_scene_tx: logic.event.Sender(.sync_self_scene),
) !void {
try sync_self_scene_tx.send(.{ .reason = .entrance });
}

View File

@@ -0,0 +1,27 @@
const std = @import("std");
const logic = @import("../logic.zig");
const meta = std.meta;
const Allocator = std.mem.Allocator;
pub fn resolve(
comptime Query: type,
world: *logic.World,
event_queue: *logic.event.Queue,
gpa: Allocator,
arena: Allocator,
) !Query {
if (comptime meta.activeTag(@typeInfo(Query)) == .@"struct") {
if (@hasDecl(Query, "allocator_kind")) {
switch (Query.allocator_kind) {
.gpa => return .{ .interface = gpa },
.arena => return .{ .interface = arena },
}
} else if (@hasDecl(Query, "tx_event_kind")) {
return .{ .event_queue = event_queue };
}
}
return world.getComponentByType(Query);
}

View File

@@ -0,0 +1,94 @@
const std = @import("std");
const logic = @import("../logic.zig");
const Session = @import("../Session.zig");
const meta = std.meta;
const event = logic.event;
const Io = std.Io;
const Allocator = std.mem.Allocator;
const namespaces = &.{
@import("systems/base.zig"),
@import("systems/game_vars.zig"),
@import("systems/unlock.zig"),
@import("systems/item_bag.zig"),
@import("systems/char_bag.zig"),
@import("systems/bitset.zig"),
@import("systems/dungeon.zig"),
@import("systems/domain_dev.zig"),
@import("systems/factory.zig"),
@import("systems/stubs.zig"),
@import("systems/friend.zig"),
@import("systems/scene.zig"),
@import("systems/player_saves.zig"),
};
pub const RunSystemsError = Io.Cancelable || Session.SendError || Allocator.Error;
// Initiate an event frame by triggering one.
pub fn triggerEvent(kind: event.Kind, world: *logic.World, gpa: Allocator) RunSystemsError!void {
var arena: std.heap.ArenaAllocator = .init(gpa); // Arena for the event frame.
defer arena.deinit();
var queue: event.Queue = .init(arena.allocator());
try queue.push(kind);
try run(world, &queue, gpa, arena.allocator());
}
// Execute the event frame.
pub fn run(world: *logic.World, queue: *event.Queue, gpa: Allocator, arena: Allocator) RunSystemsError!void {
while (queue.deque.popFront()) |event_kind| {
try dispatchEvent(event_kind, world, queue, gpa, arena);
}
}
// Process single event of the frame.
fn dispatchEvent(
kind: event.Kind,
world: *logic.World,
queue: *event.Queue,
gpa: Allocator,
arena: Allocator,
) RunSystemsError!void {
switch (kind) {
inline else => |payload, tag| inline for (namespaces) |namespace| {
inline for (@typeInfo(namespace).@"struct".decls) |decl_info| {
const decl = @field(namespace, decl_info.name);
const fn_info = switch (@typeInfo(@TypeOf(decl))) {
.@"fn" => |info| info,
else => continue,
};
if (fn_info.params.len == 0) continue;
const Param = fn_info.params[0].type.?;
if (!@hasDecl(Param, "rx_event_kind")) continue;
if (Param.rx_event_kind != tag) continue;
try invoke(payload, decl, world, queue, gpa, arena);
}
},
}
}
fn invoke(
payload: anytype,
decl: anytype,
world: *logic.World,
queue: *event.Queue,
gpa: Allocator,
arena: Allocator,
) !void {
var handler_args: meta.ArgsTuple(@TypeOf(decl)) = undefined;
handler_args[0] = .{ .payload = payload };
inline for (@typeInfo(@TypeOf(decl)).@"fn".params[1..], 1..) |param, i| {
handler_args[i] = logic.queries.resolve(param.type.?, world, queue, gpa, arena) catch {
return;
};
}
try @call(.auto, decl, handler_args);
}

View File

@@ -0,0 +1,21 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncBaseDataOnLogin(
rx: logic.event.Receiver(.login),
session: *Session,
base_comp: Player.Component(.base),
) !void {
_ = rx;
try session.send(pb.SC_SYNC_BASE_DATA{
.roleid = base_comp.data.role_id,
.role_name = base_comp.data.role_name.view(),
.level = @intFromEnum(base_comp.data.level),
.gender = @enumFromInt(@intFromEnum(base_comp.data.gender)),
.short_id = "1",
});
}

View File

@@ -0,0 +1,27 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncAllBitset(
rx: logic.event.Receiver(.login),
session: *Session,
bitset: Player.Component(.bitset),
) !void {
_ = rx;
var sync_all_bitset: pb.SC_SYNC_ALL_BITSET = .init;
var sets_buf: [Player.Bitset.Type.count]pb.BITSET_DATA = undefined;
sync_all_bitset.bitset = .initBuffer(&sets_buf);
for (&bitset.data.sets, 1..) |*set, i| {
sync_all_bitset.bitset.appendAssumeCapacity(.{
.type = @intCast(i),
.value = .{ .items = @constCast(&set.masks) },
});
}
try session.send(sync_all_bitset);
}

View File

@@ -0,0 +1,100 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Assets = @import("../../Assets.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncCharBag(
rx: logic.event.Receiver(.login),
assets: *const Assets,
session: *Session,
char_bag: Player.Component(.char_bag),
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
var sync_char_bag: pb.SC_SYNC_CHAR_BAG_INFO = .{
.curr_team_index = @intCast(char_bag.data.meta.curr_team_index),
.temp_team_info = .init,
.scope_name = 1,
.max_char_team_member_count = comptime @intCast(Player.CharBag.Team.slots_count),
};
const teams = char_bag.data.teams.slice();
try sync_char_bag.team_info.ensureTotalCapacity(arena.interface, teams.len);
const all_team_slots = try arena.interface.alloc([4]u64, teams.len);
for (
0..,
teams.items(.name),
teams.items(.char_team),
teams.items(.leader_index),
) |i, name, slots, leader_index| {
var char_team: std.ArrayList(u64) = .initBuffer(&all_team_slots[i]);
for (slots) |slot| if (slot != .empty) {
char_team.appendAssumeCapacity(@intFromEnum(slot) + 1);
};
sync_char_bag.team_info.appendAssumeCapacity(.{
.team_name = name.view(),
.char_team = .{ .items = char_team.items },
.leaderid = leader_index.objectId(),
});
}
const chars = char_bag.data.chars.slice();
try sync_char_bag.char_info.ensureTotalCapacity(arena.interface, chars.len);
for (0..chars.len) |i| {
const index: Player.CharBag.CharIndex = @enumFromInt(i);
const template_id = assets.numToStr(.char_id, chars.items(.template_id)[i]) orelse continue;
const skills = assets.char_skill_map.map.getPtr(template_id).?;
var char_info: pb.CHAR_INFO = .{
.objid = index.objectId(),
.templateid = template_id,
.char_type = .default_type,
.level = chars.items(.level)[i],
.exp = chars.items(.exp)[i],
.is_dead = chars.items(.is_dead)[i],
.weapon_id = chars.items(.weapon_id)[i].instId(),
.own_time = chars.items(.own_time)[i],
.equip_medicine_id = chars.items(.equip_medicine_id)[i],
.potential_level = chars.items(.potential_level)[i],
.normal_skill = skills.normal_skill,
.battle_info = .{ .hp = chars.items(.hp)[i], .ultimatesp = chars.items(.ultimate_sp)[i] },
.skill_info = .{
.normal_skill = skills.normal_skill,
.combo_skill = skills.combo_skill,
.ultimate_skill = skills.ultimate_skill,
.disp_normal_attack_skill = skills.attack_skill,
},
.talent = .{},
.battle_mgr_info = .{
.msg_generation = @truncate(index.objectId()),
.battle_inst_id = @truncate(index.objectId()),
.part_inst_info = .{},
},
.trial_data = .{},
};
try char_info.skill_info.?.level_info.ensureTotalCapacity(arena.interface, skills.all_skills.len);
for (skills.all_skills) |name| {
char_info.skill_info.?.level_info.appendAssumeCapacity(.{
.skill_id = name,
.skill_level = 1,
.skill_max_level = 1,
.skill_enhanced_level = 1,
});
}
try sync_char_bag.char_info.append(arena.interface, char_info);
}
try session.send(sync_char_bag);
}

View File

@@ -0,0 +1,23 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Assets = @import("../../Assets.zig");
const Session = @import("../../Session.zig");
pub fn syncDomainDevSystem(
rx: logic.event.Receiver(.login),
session: *Session,
assets: *const Assets,
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
var domain_dev_sync: pb.SC_DOMAIN_DEVELOPMENT_SYSTEM_SYNC = .init;
for (assets.table(.domain_data).keys()) |chapter_id| {
try domain_dev_sync.domains.append(arena.interface, .{
.chapter_id = chapter_id,
.dev_degree = .{ .level = 1 },
});
}
try session.send(domain_dev_sync);
}

View File

@@ -0,0 +1,16 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
pub fn syncFullDungeonStatus(
rx: logic.event.Receiver(.login),
session: *Session,
) !void {
_ = rx;
// TODO
try session.send(pb.SC_SYNC_FULL_DUNGEON_STATUS{
.cur_stamina = 200,
.max_stamina = 200,
});
}

View File

@@ -0,0 +1,53 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Assets = @import("../../Assets.zig");
const Session = @import("../../Session.zig");
const default_chapter = "domain_2";
pub fn syncFactoryData(
rx: logic.event.Receiver(.login),
session: *Session,
assets: *const Assets,
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
try session.send(pb.SC_FACTORY_SYNC{
.stt = .init,
.formula_man = .init,
.progress_status = .init,
});
var factory_sync_scope: pb.SC_FACTORY_SYNC_SCOPE = .{
.scope_name = 1,
.current_chapter_id = default_chapter,
.transport_route = .init,
.book_mark = .init,
.sign_mgr = .init,
.shared_mgr = .init,
};
for (assets.table(.domain_data).keys()) |chapter_id| {
try factory_sync_scope.active_chapter_ids.append(arena.interface, chapter_id);
}
try session.send(factory_sync_scope);
for (assets.table(.domain_data).keys()) |chapter_id| {
try session.send(pb.SC_FACTORY_SYNC_CHAPTER{
.chapter_id = chapter_id,
.blackboard = .{ .power = .{ .is_stop_by_power = true } },
.pin_board = .{},
.statistic = .{},
.pending_place = .{},
});
try session.send(pb.SC_FACTORY_HS{
.blackboard = .{
.power = .{ .is_stop_by_power = true },
},
.chapter_id = chapter_id,
});
}
}

View File

@@ -0,0 +1,19 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
pub fn syncPersonalData(
rx: logic.event.Receiver(.login),
session: *Session,
) !void {
_ = rx;
// TODO
try session.send(pb.SC_FRIEND_PERSONAL_DATA_SYNC{
.data = .{
.user_avatar_id = 7,
.User_avatar_frame_id = 3,
.business_card_topic_id = 11,
},
});
}

View File

@@ -0,0 +1,32 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncAllGameVars(
rx: logic.event.Receiver(.login),
session: *Session,
game_vars: Player.Component(.game_vars),
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
var sync_all_game_var: pb.SC_SYNC_ALL_GAME_VAR = .init;
try sync_all_game_var.server_vars.ensureTotalCapacity(arena.interface, game_vars.data.server_vars.len);
try sync_all_game_var.client_vars.ensureTotalCapacity(arena.interface, game_vars.data.client_vars.len);
for (game_vars.data.server_vars) |sv| {
sync_all_game_var.server_vars.appendAssumeCapacity(
.{ .key = @intFromEnum(sv.key), .value = sv.value },
);
}
for (game_vars.data.client_vars) |cv| {
sync_all_game_var.client_vars.appendAssumeCapacity(
.{ .key = cv.key, .value = cv.value },
);
}
try session.send(sync_all_game_var);
}

View File

@@ -0,0 +1,55 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncItemBagScopes(
rx: logic.event.Receiver(.login),
session: *Session,
item_bag: Player.Component(.item_bag),
char_bag: Player.Component(.char_bag),
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
var item_bag_scope_sync: pb.SC_ITEM_BAG_SCOPE_SYNC = .{
.bag = .init,
.quick_bar = .init,
.assistant = .init,
.scope_name = 1,
};
var weapon_depot: pb.SCD_ITEM_DEPOT = .init;
try weapon_depot.inst_list.ensureTotalCapacity(arena.interface, item_bag.data.weapon_depot.len);
const weapon_slice = item_bag.data.weapon_depot.slice();
for (0..weapon_slice.len) |i| {
const weapon_index: Player.ItemBag.WeaponIndex = @enumFromInt(i);
const weapon = weapon_slice.get(i);
weapon_depot.inst_list.appendAssumeCapacity(.{
.count = 1,
.inst = .{
.inst_id = weapon_index.instId(),
.inst_impl = .{ .weapon = .{
.inst_id = weapon_index.instId(),
.template_id = weapon.template_id,
.exp = weapon.exp,
.weapon_lv = weapon.weapon_lv,
.refine_lv = weapon.refine_lv,
.breakthrough_lv = weapon.breakthrough_lv,
.attach_gem_id = weapon.attach_gem_id,
.equip_char_id = if (char_bag.data.charIndexWithWeapon(weapon_index)) |char_index|
char_index.objectId()
else
0,
} },
},
});
}
try item_bag_scope_sync.depot.append(arena.interface, .{ .key = 1, .value = weapon_depot });
try session.send(item_bag_scope_sync);
}

View File

@@ -0,0 +1,38 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const fs = @import("../../fs.zig");
const Io = std.Io;
const Player = logic.Player;
const log = std.log.scoped(.player_saves);
pub fn saveCharBagTeams(
_: logic.event.Receiver(.char_bag_team_modified),
char_bag: Player.Component(.char_bag),
player_id: logic.World.PlayerId,
io: Io,
) !void {
const data_dir = fs.persistence.openPlayerDataDir(io, player_id.uid) 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 },
);
return;
},
};
defer data_dir.close(io);
fs.persistence.saveCharBagComponent(io, data_dir, char_bag.data, .teams) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("save failed: {t}", .{e});
return;
},
};
}

View File

@@ -0,0 +1,180 @@
const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Assets = @import("../../Assets.zig");
const Player = logic.Player;
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;
const default_level = "map02_lv001";
pub fn enterSceneOnLogin(
rx: logic.event.Receiver(.login),
session: *Session,
assets: *const Assets,
base_comp: Player.Component(.base),
) !void {
_ = rx;
const level_config = assets.level_config_table.getPtr(default_level).?;
const position: pb.VECTOR = .{
.X = level_config.playerInitPos.x,
.Y = level_config.playerInitPos.y,
.Z = level_config.playerInitPos.z,
};
try session.send(pb.SC_CHANGE_SCENE_BEGIN_NOTIFY{
.scene_num_id = level_config.idNum,
.position = position,
.pass_through_data = .init,
});
try session.send(pb.SC_ENTER_SCENE_NOTIFY{
.role_id = base_comp.data.role_id,
.scene_num_id = level_config.idNum,
.position = position,
.pass_through_data = .init,
});
}
pub fn refreshCharTeam(
rx: logic.event.Receiver(.char_bag_team_modified),
char_bag: Player.Component(.char_bag),
sync_tx: logic.event.Sender(.sync_self_scene),
) !void {
switch (rx.payload.modification) {
.set_leader => return, // Doesn't require any action from server.
.set_char_team => if (rx.payload.team_index == char_bag.data.meta.curr_team_index) {
// If the current active team has been modified, it has to be re-spawned.
try sync_tx.send(.{ .reason = .team_modified });
},
}
}
pub fn syncSelfScene(
rx: logic.event.Receiver(.sync_self_scene),
session: *Session,
arena: logic.Resource.Allocator(.arena),
char_bag: logic.Player.Component(.char_bag),
assets: *const Assets,
) !void {
const reason: pb.SELF_INFO_REASON_TYPE = switch (rx.payload.reason) {
.entrance => .SLR_ENTER_SCENE,
.team_modified => .SLR_CHANGE_TEAM,
};
const level_config = assets.level_config_table.getPtr(default_level).?;
const position: pb.VECTOR = .{
.X = level_config.playerInitPos.x,
.Y = level_config.playerInitPos.y,
.Z = level_config.playerInitPos.z,
};
const team_index = char_bag.data.meta.curr_team_index;
const leader_index = char_bag.data.teams.items(.leader_index)[team_index];
var self_scene_info: pb.SC_SELF_SCENE_INFO = .{
.scene_num_id = level_config.idNum,
.self_info_reason = @intFromEnum(reason),
.teamInfo = .{
.team_type = .CHAR_BAG_TEAM_TYPE_MAIN,
.team_index = @intCast(team_index),
.cur_leader_id = leader_index.objectId(),
.team_change_token = 0,
},
.scene_impl = .{ .empty = .{} },
.detail = .{},
};
for (char_bag.data.teams.items(.char_team)[team_index]) |slot| {
const char_index = slot.charIndex() orelse continue;
const char_template_id_num = char_bag.data.chars.items(.template_id)[@intFromEnum(char_index)];
const char_template_id = assets.numToStr(.char_id, char_template_id_num).?;
const char_data = assets.table(.character).getPtr(char_template_id).?;
var scene_char: pb.SCENE_CHARACTER = .{
.level = 1,
.battle_info = .{
.msg_generation = @intCast(char_index.objectId()),
.battle_inst_id = @intCast(char_index.objectId()),
.part_inst_info = .{},
},
.common_info = .{
.id = char_index.objectId(),
.templateid = char_template_id,
.position = position,
.rotation = .{},
.scene_num_id = level_config.idNum,
},
};
for (char_data.attributes[0].Attribute.attrs) |attr| {
if (attr.attrType == .max_hp)
scene_char.common_info.?.hp = attr.attrValue;
try scene_char.attrs.append(arena.interface, .{
.attr_type = @intFromEnum(attr.attrType),
.basic_value = attr.attrValue,
.value = attr.attrValue,
});
}
scene_char.battle_info.?.skill_list = try packCharacterSkills(
arena.interface,
assets,
char_template_id,
);
try self_scene_info.detail.?.char_list.append(arena.interface, scene_char);
}
try session.send(self_scene_info);
}
fn packCharacterSkills(
arena: Allocator,
assets: *const Assets,
template_id: []const u8,
) Allocator.Error!ArrayList(pb.SERVER_SKILL) {
const char_skills = assets.char_skill_map.map.getPtr(template_id).?.all_skills;
var list: ArrayList(pb.SERVER_SKILL) = try .initCapacity(
arena,
char_skills.len + assets.common_skill_config.config.Character.skillConfigs.len,
);
errdefer comptime unreachable;
for (char_skills, 1..) |name, i| {
list.appendAssumeCapacity(.{
.skill_id = .{
.id_impl = .{ .str_id = name },
.type = .BATTLE_ACTION_OWNER_TYPE_SKILL,
},
.blackboard = .{},
.inst_id = (100 + i),
.level = 1,
.source = .BATTLE_SKILL_SOURCE_DEFAULT,
.potential_lv = 1,
.is_enable = true,
});
}
for (assets.common_skill_config.config.Character.skillConfigs, char_skills.len + 1..) |config, i| {
list.appendAssumeCapacity(.{
.skill_id = .{
.id_impl = .{ .str_id = config.skillId },
.type = .BATTLE_ACTION_OWNER_TYPE_SKILL,
},
.blackboard = .{},
.inst_id = (100 + i),
.level = 1,
.source = .BATTLE_SKILL_SOURCE_DEFAULT,
.potential_lv = 1,
.is_enable = true,
});
}
return list;
}

View File

@@ -0,0 +1,50 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
// Sends the dummy 'SYNC' messages for the components that aren't implemented yet.
pub fn loginSyncStub(
rx: logic.event.Receiver(.login),
session: *Session,
) !void {
_ = rx;
try session.send(pb.SC_ADVENTURE_SYNC_ALL{
.level = 1,
.world_level = 1,
.unlock_world_level = 1,
});
try session.send(pb.SC_ADVENTURE_BOOK_SYNC{
.adventure_book_stage = 1,
});
try session.send(pb.SC_SYNC_ALL_MINI_GAME.init);
try session.send(pb.SC_SYNC_ALL_MAIL.init);
try session.send(pb.SC_KITE_STATION_SYNC_ALL.init);
try session.send(pb.SC_SYNC_ALL_GUIDE.init);
try session.send(pb.SC_GLOBAL_EFFECT_SYNC_ALL.init);
try session.send(pb.SC_SYNC_ALL_DOODAD_GROUP.init);
try session.send(pb.SC_SETTLEMENT_SYNC_ALL.init);
try session.send(pb.SC_DOMAIN_DEPOT_SYNC_ALL_INFO.init);
try session.send(pb.SC_SYNC_ALL_DIALOG.init);
try session.send(pb.SC_SYNC_ALL_ROLE_SCENE.init);
try session.send(pb.SC_SYNC_ALL_WIKI.init);
try session.send(pb.SC_RECYCLE_BIN_SYSTEM_SYNC_ALL.init);
try session.send(pb.SC_SYNC_ALL_STAT.init);
try session.send(pb.SC_BP_SYNC_ALL{
.season_data = .init,
.level_data = .init,
.bp_track_mgr = .init,
.bp_task_mgr = .init,
});
try session.send(pb.SC_SYNC_ALL_MISSION.init);
try session.send(pb.SC_SPACESHIP_SYNC{
.assist_data = .init,
.expedition_data = .init,
});
try session.send(pb.SC_ACHIEVE_SYNC{
.achieve_display_info = .init,
});
}

View File

@@ -0,0 +1,22 @@
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Player = logic.Player;
pub fn syncAllUnlock(
rx: logic.event.Receiver(.login),
session: *Session,
unlock: Player.Component(.unlock),
arena: logic.Resource.Allocator(.arena),
) !void {
_ = rx;
var sync_all_unlock: pb.SC_SYNC_ALL_UNLOCK = .init;
try sync_all_unlock.unlock_systems.appendSlice(
arena.interface,
@ptrCast(unlock.data.unlocked_systems),
);
try session.send(sync_all_unlock);
}

140
gamesv/src/main.zig Normal file
View File

@@ -0,0 +1,140 @@
const std = @import("std");
const common = @import("common");
const Session = @import("Session.zig");
const Assets = @import("Assets.zig");
const Io = std.Io;
const Init = std.process.Init;
const Allocator = std.mem.Allocator;
const net = Io.net;
const assert = std.debug.assert;
const log = std.log.scoped(.gamesv);
const Options = struct {
listen_address: []const u8 = "127.0.0.1:30000",
};
fn start(init: Init.Minimal, io: Io, gpa: Allocator) u8 {
const args = common.args.parseOrPrintUsageAlloc(Options, gpa, init.args) orelse return 1;
defer args.deinit();
std.debug.print(
\\ __ ____ _____
\\ / / / __ \ / ___/
\\ / / / /_/ /_____\__ \
\\ / /___/ _, _/_____/__/ /
\\/_____/_/ |_| /____/
\\
, .{});
var assets: Assets = loadAssetsMultithreaded(gpa) catch |err| switch (err) {
error.Canceled => return 0, // got interrupted early
else => |e| {
log.err("failed to load assets: {t}", .{e});
return 1;
},
};
defer assets.deinit();
const listen_address = net.IpAddress.parseLiteral(args.options.listen_address) catch {
log.err("Invalid listen address specified.", .{});
return 1;
};
var server = listen_address.listen(io, .{ .reuse_address = true }) catch |err| switch (err) {
error.AddressInUse => {
log.err(
"Address '{f}' is in use. Another instance of this server might be already running.",
.{listen_address},
);
return 1;
},
else => |e| {
log.err("Failed to listen at '{f}': {t}", .{ listen_address, e });
return 1;
},
};
defer server.deinit(io);
var sessions: Io.Group = .init;
defer sessions.cancel(io);
var preferred_clock: Io.Clock = .awake;
var concurrency_availability: Session.ConcurrencyAvailability = .undetermined;
log.info("listening at {f}", .{listen_address});
defer log.info("shutting down...", .{});
accept_loop: while (true) {
const stream = server.accept(io) catch |err| switch (err) {
error.Canceled => break, // Shutdown requested
error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, error.SystemResources => {
// System is overloaded. Stop accepting new connections for now.
while (true) {
if (io.sleep(.fromSeconds(1), preferred_clock)) break else |sleep_err| switch (sleep_err) {
error.Canceled => break :accept_loop, // Shutdown requested
error.UnsupportedClock => preferred_clock = if (preferred_clock == .awake)
.real
else
continue :accept_loop, // No clock available.
error.Unexpected => continue :accept_loop, // Sleep is unimportant then.
}
}
continue;
},
else => |e| { // Something else happened. We probably want to report this and continue.
log.err("TCP accept failed: {t}", .{e});
continue;
},
};
var io_options: Session.IoOptions = .{
.preferred_clock = preferred_clock,
.concurrency = .available,
};
if (sessions.concurrent(io, Session.process, .{ io, gpa, &assets, stream, io_options })) {
concurrency_availability = .available;
} else |err| switch (err) {
error.ConcurrencyUnavailable => switch (concurrency_availability) {
.available => stream.close(io), // Can't process more connections atm.
.unavailable, .undetermined => {
// The environment doesn't support concurrency.
if (concurrency_availability != .unavailable)
log.warn("Environment doesn't support concurrency. One session at a time will be processed.", .{});
concurrency_availability = .unavailable;
io_options.concurrency = .unavailable;
sessions.async(io, Session.process, .{ io, gpa, &assets, stream, io_options });
},
},
}
}
return 0;
}
// Loads assets using Io.Threaded under the hood
fn loadAssetsMultithreaded(gpa: Allocator) !Assets {
var threaded: Io.Threaded = .init(gpa, .{ .environ = .empty });
defer threaded.deinit();
return try Assets.load(threaded.io(), gpa);
}
pub fn main(init: Init.Minimal) u8 {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer assert(.ok == debug_allocator.deinit());
const gpa = debug_allocator.allocator();
var poll: common.io.Poll(.{}) = .init(gpa);
defer poll.deinit();
const io = poll.io();
return start(init, io, gpa);
}

45
gamesv/src/network.zig Normal file
View File

@@ -0,0 +1,45 @@
const std = @import("std");
const proto = @import("proto");
const pb = proto.pb;
const Io = std.Io;
const Crc32 = std.hash.Crc32;
pub const Request = struct {
head: pb.CSHead,
body: []u8,
pub const ReadError = error{
HeadDecodeError,
ChecksumMismatch,
InvalidMessageId,
} || Io.Reader.Error;
pub fn read(reader: *Io.Reader) ReadError!Request {
const header_length = try reader.takeInt(u8, .little);
const body_length = try reader.takeInt(u16, .little);
const payload = try reader.take(header_length + body_length);
var head_reader: Io.Reader = .fixed(payload[0..header_length]);
const head = proto.decodeMessage(
&head_reader,
.failing, // CSHead contains only scalar fields. No allocation needed.
pb.CSHead,
) catch return error.HeadDecodeError;
if (std.enums.fromInt(pb.CSMessageID, head.msgid) == null)
return error.InvalidMessageId;
const body = payload[header_length..];
const checksum = Crc32.hash(body);
if (checksum != head.checksum)
return error.ChecksumMismatch;
return .{ .head = head, .body = body };
}
pub fn msgId(request: *const Request) pb.CSMessageID {
return @enumFromInt(request.head.msgid);
}
};