track state of inventory locally
This commit is contained in:
parent
72d70c5d43
commit
99aad39c57
@ -10,6 +10,9 @@ pub fn build(b: *std.Build) void {
|
|||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
exe.linkLibC();
|
||||||
|
exe.addIncludePath(b.path("src"));
|
||||||
|
exe.addCSourceFile(.{ .file = b.path("src/timegm.c") });
|
||||||
|
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
@ -3,31 +3,44 @@ const assert = std.debug.assert;
|
|||||||
const json = std.json;
|
const json = std.json;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const json_utils = @import("json_utils.zig");
|
||||||
|
pub const Character = @import("character.zig");
|
||||||
|
|
||||||
// Specification: https://api.artifactsmmo.com/docs
|
// Specification: https://api.artifactsmmo.com/docs
|
||||||
|
|
||||||
// TODO: Convert 'expiration' date time strings into date time objects
|
|
||||||
|
|
||||||
const ArtifactsAPI = @This();
|
const ArtifactsAPI = @This();
|
||||||
|
|
||||||
|
pub const ItemId = u32;
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
client: std.http.Client,
|
||||||
|
|
||||||
|
server: []u8,
|
||||||
|
server_uri: std.Uri,
|
||||||
|
|
||||||
|
token: ?[]u8 = null,
|
||||||
|
|
||||||
|
item_codes: std.ArrayList([]u8),
|
||||||
|
|
||||||
pub const APIErrors = error {
|
pub const APIErrors = error {
|
||||||
RequestFailed,
|
RequestFailed,
|
||||||
ParseFailed
|
ParseFailed
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServerStatus = struct {
|
const ServerStatus = struct {
|
||||||
// TODO: Parse the rest of the fields
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
status: []const u8,
|
status: []const u8,
|
||||||
version: []const u8,
|
version: []const u8,
|
||||||
|
characters_online: i64,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, object: json.ObjectMap) !ServerStatus {
|
fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
|
||||||
const status = getJsonString(object, "status") orelse return error.MissingStatus;
|
_ = api;
|
||||||
const version = getJsonString(object, "version") orelse return error.MissingVersion;
|
|
||||||
|
|
||||||
return ServerStatus{
|
return ServerStatus{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.status = try allocator.dupe(u8, status),
|
.characters_online = json_utils.getInteger(object, "characters_online") orelse return error.MissingProperty,
|
||||||
.version = try allocator.dupe(u8, version)
|
.status = (try json_utils.dupeString(allocator, object, "status")) orelse return error.MissingStatus,
|
||||||
|
.version = (try json_utils.dupeString(allocator, object, "version")) orelse return error.MissingVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,211 +50,42 @@ const ServerStatus = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Character = struct {
|
pub fn parseDateTime(datetime: []const u8) ?f64 {
|
||||||
pub const SkillStats = struct {
|
const time_h = @cImport({
|
||||||
level: i64,
|
@cDefine("_XOPEN_SOURCE", "700");
|
||||||
xp: i64,
|
@cInclude("stddef.h");
|
||||||
max_xp: i64,
|
@cInclude("stdio.h");
|
||||||
|
@cInclude("time.h");
|
||||||
|
@cInclude("timegm.h");
|
||||||
|
});
|
||||||
|
|
||||||
fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
|
var buffer: [256]u8 = undefined;
|
||||||
return SkillStats{
|
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||||
.level = getJsonInteger(object, level) orelse return error.MissingProperty,
|
var allocator = fba.allocator();
|
||||||
.xp = getJsonInteger(object, xp) orelse return error.MissingProperty,
|
|
||||||
.max_xp = getJsonInteger(object, max_xp) orelse return error.MissingProperty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CombatStats = struct {
|
const datetime_z = allocator.dupeZ(u8, datetime) catch return null;
|
||||||
attack: i64,
|
|
||||||
damage: i64,
|
|
||||||
resistance: i64,
|
|
||||||
|
|
||||||
fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
|
var tm: time_h.tm = .{};
|
||||||
return CombatStats{
|
const s = time_h.strptime(datetime_z, "%FT%H:%M:%S.", &tm);
|
||||||
.attack = getJsonInteger(object, attack) orelse return error.MissingProperty,
|
if (s == null) {
|
||||||
.damage = getJsonInteger(object, damage) orelse return error.MissingProperty,
|
return null;
|
||||||
.resistance = getJsonInteger(object, resistance) orelse return error.MissingProperty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Equipment = struct {
|
|
||||||
pub const Consumable = struct {
|
|
||||||
name: []u8,
|
|
||||||
quantity: i64,
|
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
|
|
||||||
return Consumable{
|
|
||||||
.name = (try dupeJsonString(allocator, obj, name)) orelse return error.MissingProperty,
|
|
||||||
.quantity = getJsonInteger(obj, quantity) orelse return error.MissingProperty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
weapon: []u8,
|
|
||||||
shield: []u8,
|
|
||||||
helmet: []u8,
|
|
||||||
body_armor: []u8,
|
|
||||||
leg_armor: []u8,
|
|
||||||
boots: []u8,
|
|
||||||
|
|
||||||
ring1: []u8,
|
|
||||||
ring2: []u8,
|
|
||||||
amulet: []u8,
|
|
||||||
|
|
||||||
artifact1: []u8,
|
|
||||||
artifact2: []u8,
|
|
||||||
artifact3: []u8,
|
|
||||||
|
|
||||||
consumable1: Consumable,
|
|
||||||
consumable2: Consumable,
|
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !Equipment {
|
|
||||||
return Equipment{
|
|
||||||
.weapon = (try dupeJsonString(allocator, obj, "weapon_slot")) orelse return error.MissingProperty,
|
|
||||||
.shield = (try dupeJsonString(allocator, obj, "shield_slot")) orelse return error.MissingProperty,
|
|
||||||
.helmet = (try dupeJsonString(allocator, obj, "helmet_slot")) orelse return error.MissingProperty,
|
|
||||||
.body_armor = (try dupeJsonString(allocator, obj, "body_armor_slot")) orelse return error.MissingProperty,
|
|
||||||
.leg_armor = (try dupeJsonString(allocator, obj, "leg_armor_slot")) orelse return error.MissingProperty,
|
|
||||||
.boots = (try dupeJsonString(allocator, obj, "boots_slot")) orelse return error.MissingProperty,
|
|
||||||
.ring1 = (try dupeJsonString(allocator, obj, "ring1_slot")) orelse return error.MissingProperty,
|
|
||||||
.ring2 = (try dupeJsonString(allocator, obj, "ring2_slot")) orelse return error.MissingProperty,
|
|
||||||
.amulet = (try dupeJsonString(allocator, obj, "amulet_slot")) orelse return error.MissingProperty,
|
|
||||||
.artifact1 = (try dupeJsonString(allocator, obj, "artifact1_slot")) orelse return error.MissingProperty,
|
|
||||||
.artifact2 = (try dupeJsonString(allocator, obj, "artifact2_slot")) orelse return error.MissingProperty,
|
|
||||||
.artifact3 = (try dupeJsonString(allocator, obj, "artifact3_slot")) orelse return error.MissingProperty,
|
|
||||||
.consumable1 = try Consumable.parse(allocator, obj, "consumable1_slot", "consumable1_slot_quantity"),
|
|
||||||
.consumable2 = try Consumable.parse(allocator, obj, "consumable2_slot", "consumable2_slot_quantity"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Inventory = struct {
|
|
||||||
const slots_count = 20;
|
|
||||||
|
|
||||||
pub const InventorySlot = struct {
|
|
||||||
code: []const u8,
|
|
||||||
quantity: i64,
|
|
||||||
|
|
||||||
fn parse(allocator: Allocator, slot_obj: json.ObjectMap) !InventorySlot {
|
|
||||||
return InventorySlot{
|
|
||||||
.code = (try dupeJsonString(allocator, slot_obj, "code")) orelse return error.MissingProperty,
|
|
||||||
.quantity = getJsonInteger(slot_obj, "quantity") orelse return error.MissingProperty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
slots: [slots_count]InventorySlot,
|
|
||||||
|
|
||||||
fn parse(allocator: Allocator, slots_array: json.Array) !Inventory {
|
|
||||||
assert(slots_array.items.len == Inventory.slots_count);
|
|
||||||
|
|
||||||
var inventory: Inventory = undefined;
|
|
||||||
|
|
||||||
for (0.., slots_array.items) |i, slot_value| {
|
|
||||||
const slot_obj = asJsonObject(slot_value) orelse return error.InvalidType;
|
|
||||||
inventory.slots[i] = try InventorySlot.parse(allocator, slot_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inventory;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
arena: *std.heap.ArenaAllocator,
|
|
||||||
|
|
||||||
name: []u8,
|
|
||||||
skin: []u8,
|
|
||||||
account: ?[]u8,
|
|
||||||
total_xp: i64,
|
|
||||||
gold: i64,
|
|
||||||
hp: i64,
|
|
||||||
haste: i64,
|
|
||||||
x: i64,
|
|
||||||
y: i64,
|
|
||||||
cooldown: i64,
|
|
||||||
cooldown_expiration: []u8,
|
|
||||||
|
|
||||||
combat: SkillStats,
|
|
||||||
mining: SkillStats,
|
|
||||||
woodcutting: SkillStats,
|
|
||||||
fishing: SkillStats,
|
|
||||||
weaponcrafting: SkillStats,
|
|
||||||
gearcrafting: SkillStats,
|
|
||||||
jewelrycrafting: SkillStats,
|
|
||||||
cooking: SkillStats,
|
|
||||||
|
|
||||||
water: CombatStats,
|
|
||||||
fire: CombatStats,
|
|
||||||
earth: CombatStats,
|
|
||||||
air: CombatStats,
|
|
||||||
|
|
||||||
equipment: Equipment,
|
|
||||||
|
|
||||||
inventory_max_items: i64,
|
|
||||||
inventory: Inventory,
|
|
||||||
|
|
||||||
fn parse(child_allocator: Allocator, obj: json.ObjectMap) !Character {
|
|
||||||
var arena = try child_allocator.create(std.heap.ArenaAllocator);
|
|
||||||
errdefer child_allocator.destroy(arena);
|
|
||||||
|
|
||||||
arena.* = std.heap.ArenaAllocator.init(child_allocator);
|
|
||||||
errdefer arena.deinit();
|
|
||||||
|
|
||||||
const allocator = arena.allocator();
|
|
||||||
|
|
||||||
const inventory = getJsonArray(obj, "inventory") orelse return error.MissingProperty;
|
|
||||||
|
|
||||||
return Character{
|
|
||||||
.arena = arena,
|
|
||||||
.account = try dupeJsonString(allocator, obj, "account"),
|
|
||||||
.name = (try dupeJsonString(allocator, obj, "name")) orelse return error.MissingProperty,
|
|
||||||
.skin = (try dupeJsonString(allocator, obj, "skin")) orelse return error.MissingProperty,
|
|
||||||
|
|
||||||
.total_xp = getJsonInteger(obj, "total_xp") orelse return error.MissingProperty,
|
|
||||||
.gold = getJsonInteger(obj, "gold") orelse return error.MissingProperty,
|
|
||||||
.hp = getJsonInteger(obj, "hp") orelse return error.MissingProperty,
|
|
||||||
.haste = getJsonInteger(obj, "haste") orelse return error.MissingProperty,
|
|
||||||
.x = getJsonInteger(obj, "x") orelse return error.MissingProperty,
|
|
||||||
.y = getJsonInteger(obj, "y") orelse return error.MissingProperty,
|
|
||||||
.cooldown = getJsonInteger(obj, "cooldown") orelse return error.MissingProperty,
|
|
||||||
.cooldown_expiration = (try dupeJsonString(allocator, obj, "cooldown_expiration")) orelse return error.MissingProperty,
|
|
||||||
|
|
||||||
.combat = try SkillStats.parse(obj, "level", "xp", "max_xp"),
|
|
||||||
.mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"),
|
|
||||||
.woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"),
|
|
||||||
.fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"),
|
|
||||||
.weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"),
|
|
||||||
.gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"),
|
|
||||||
.jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"),
|
|
||||||
.cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"),
|
|
||||||
|
|
||||||
.water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"),
|
|
||||||
.fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
|
|
||||||
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
|
|
||||||
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
|
|
||||||
|
|
||||||
.equipment = try Equipment.parse(allocator, obj),
|
|
||||||
|
|
||||||
.inventory_max_items = getJsonInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
|
|
||||||
.inventory = try Inventory.parse(allocator, inventory)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Character) void {
|
const s_len = std.mem.len(s);
|
||||||
var child_allocator = self.arena.child_allocator;
|
if (s[s_len-1] != 'Z') {
|
||||||
self.arena.deinit();
|
return null;
|
||||||
child_allocator.destroy(self.arena);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getItemCount(self: *const Character) u32 {
|
const milliseconds_str = s[0..(s_len-1)];
|
||||||
var count: u32 = 0;
|
var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null);
|
||||||
for (self.inventory.slots) |slot| {
|
while (milliseconds >= 1) {
|
||||||
count += @intCast(slot.quantity);
|
milliseconds /= 10;
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm));
|
||||||
|
|
||||||
|
return seconds + milliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
pub const CharacterList = struct {
|
pub const CharacterList = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
@ -305,44 +149,81 @@ pub const Cooldown = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
allocator: Allocator,
|
expiration: f64,
|
||||||
total_seconds: i64,
|
|
||||||
remaining_seconds: i64,
|
|
||||||
expiration: []u8,
|
|
||||||
reason: Reason,
|
reason: Reason,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !Cooldown {
|
fn parse(obj: json.ObjectMap) !Cooldown {
|
||||||
const reason = getJsonString(obj, "reason") orelse return error.MissingProperty;
|
const reason = try json_utils.getStringRequired(obj, "reason");
|
||||||
|
const expiration = try json_utils.getStringRequired(obj, "expiration");
|
||||||
|
|
||||||
return Cooldown{
|
return Cooldown{
|
||||||
.allocator = allocator,
|
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
|
||||||
.total_seconds = getJsonInteger(obj, "total_seconds") orelse return error.MissingProperty,
|
|
||||||
.remaining_seconds = getJsonInteger(obj, "remaining_seconds") orelse return error.MissingProperty,
|
|
||||||
.expiration = (try dupeJsonString(allocator, obj, "expiration")) orelse return error.MissingProperty,
|
|
||||||
.reason = Reason.parse(reason) orelse return error.UnknownReason
|
.reason = Reason.parse(reason) orelse return error.UnknownReason
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: Cooldown) void {
|
|
||||||
self.allocator.free(self.expiration);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const FightResult = struct {
|
pub const FightResult = struct {
|
||||||
|
const Details = struct {
|
||||||
|
const DroppedItem = struct {
|
||||||
|
id: ItemId,
|
||||||
|
quantity: i64
|
||||||
|
};
|
||||||
|
|
||||||
|
const Result = enum { win, lose };
|
||||||
|
|
||||||
|
xp: i64,
|
||||||
|
gold: i64,
|
||||||
|
drops: std.BoundedArray(DroppedItem, 8),
|
||||||
|
result: Result,
|
||||||
|
|
||||||
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Details {
|
||||||
|
const result = try json_utils.getStringRequired(obj, "result");
|
||||||
|
var result_enum: Result = undefined;
|
||||||
|
if (std.mem.eql(u8, result, "win")) {
|
||||||
|
result_enum = .win;
|
||||||
|
} else if (std.mem.eql(u8, result, "win")) {
|
||||||
|
result_enum = .lose;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drops = std.BoundedArray(DroppedItem, 8).init(0) catch unreachable;
|
||||||
|
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
||||||
|
for (drops_obj.items) |drop_value| {
|
||||||
|
const drop_obj = json_utils.asObject(drop_value) orelse return error.MissingProperty;
|
||||||
|
const code = try json_utils.getStringRequired(drop_obj, "code");
|
||||||
|
|
||||||
|
try drops.append(DroppedItem{
|
||||||
|
.id = try api.getItemId(code),
|
||||||
|
.quantity = try json_utils.getIntegerRequired(drop_obj, "quantity")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Details{
|
||||||
|
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
||||||
|
.gold = try json_utils.getIntegerRequired(obj, "gold"),
|
||||||
|
.drops = drops,
|
||||||
|
.result = result_enum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cooldown: Cooldown,
|
cooldown: Cooldown,
|
||||||
character: Character,
|
character: Character,
|
||||||
|
fight: Details,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !FightResult {
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !FightResult {
|
||||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||||
const character = getJsonObject(obj, "character") orelse return error.MissingProperty;
|
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
|
||||||
|
const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty;
|
||||||
|
|
||||||
return FightResult{
|
return FightResult{
|
||||||
.cooldown = try Cooldown.parse(allocator, cooldown),
|
.cooldown = try Cooldown.parse(cooldown),
|
||||||
.character = try Character.parse(allocator, character)
|
.character = try Character.parse(character, allocator, api),
|
||||||
|
.fight = try Details.parse(api, fight)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *FightResult) void {
|
pub fn deinit(self: *FightResult) void {
|
||||||
self.cooldown.deinit();
|
|
||||||
self.character.deinit();
|
self.character.deinit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -350,48 +231,52 @@ pub const FightResult = struct {
|
|||||||
pub const MoveResult = struct {
|
pub const MoveResult = struct {
|
||||||
cooldown: Cooldown,
|
cooldown: Cooldown,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !MoveResult {
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult {
|
||||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
_ = api;
|
||||||
|
|
||||||
|
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||||
|
|
||||||
return MoveResult{
|
return MoveResult{
|
||||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
.cooldown = try Cooldown.parse(cooldown)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: MoveResult) void {
|
pub fn deinit(self: MoveResult) void {
|
||||||
self.cooldown.deinit();
|
_ = self;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const GoldTransactionResult = struct {
|
pub const GoldTransactionResult = struct {
|
||||||
cooldown: Cooldown,
|
cooldown: Cooldown,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !GoldTransactionResult {
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult {
|
||||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
_ = api;
|
||||||
|
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||||
|
|
||||||
return GoldTransactionResult{
|
return GoldTransactionResult{
|
||||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
.cooldown = try Cooldown.parse(cooldown)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: GoldTransactionResult) void {
|
pub fn deinit(self: GoldTransactionResult) void {
|
||||||
self.cooldown.deinit();
|
_ = self;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ItemTransactionResult = struct {
|
pub const ItemTransactionResult = struct {
|
||||||
cooldown: Cooldown,
|
cooldown: Cooldown,
|
||||||
|
|
||||||
fn parse(allocator: Allocator, obj: json.ObjectMap) !ItemTransactionResult {
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult {
|
||||||
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
|
_ = api;
|
||||||
|
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
|
||||||
|
|
||||||
return ItemTransactionResult{
|
return ItemTransactionResult{
|
||||||
.cooldown = try Cooldown.parse(allocator, cooldown)
|
.cooldown = try Cooldown.parse(cooldown)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: ItemTransactionResult) void {
|
pub fn deinit(self: ItemTransactionResult) void {
|
||||||
self.cooldown.deinit();
|
_ = self;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -405,26 +290,30 @@ pub const ArtifactsFetchResult = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
allocator: Allocator,
|
|
||||||
client: std.http.Client,
|
|
||||||
|
|
||||||
server: []u8,
|
|
||||||
server_uri: std.Uri,
|
|
||||||
|
|
||||||
token: ?[]u8 = null,
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator) !ArtifactsAPI {
|
pub fn init(allocator: Allocator) !ArtifactsAPI {
|
||||||
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com");
|
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com");
|
||||||
const server_uri = std.Uri.parse(server) catch unreachable;
|
const server_uri = std.Uri.parse(server) catch unreachable;
|
||||||
|
|
||||||
return ArtifactsAPI{
|
return ArtifactsAPI{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.item_codes = std.ArrayList([]u8).init(allocator),
|
||||||
.client = .{ .allocator = allocator },
|
.client = .{ .allocator = allocator },
|
||||||
.server = server,
|
.server = server,
|
||||||
.server_uri = server_uri
|
.server_uri = server_uri
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ArtifactsAPI) void {
|
||||||
|
self.client.deinit();
|
||||||
|
self.allocator.free(self.server);
|
||||||
|
if (self.token) |str| self.allocator.free(str);
|
||||||
|
|
||||||
|
for (self.item_codes.items) |code| {
|
||||||
|
self.allocator.free(code);
|
||||||
|
}
|
||||||
|
self.item_codes.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult {
|
fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult {
|
||||||
var uri = self.server_uri;
|
var uri = self.server_uri;
|
||||||
uri.path = .{ .raw = path };
|
uri.path = .{ .raw = path };
|
||||||
@ -472,7 +361,7 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8) !Result {
|
fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8, args: anytype) !Result {
|
||||||
const result = try self.fetch(method, path, payload);
|
const result = try self.fetch(method, path, payload);
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
|
|
||||||
@ -483,8 +372,9 @@ fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Metho
|
|||||||
return APIErrors.ParseFailed;
|
return APIErrors.ParseFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
|
const body = json_utils.asObject(result.body.?) orelse return APIErrors.ParseFailed;
|
||||||
return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed;
|
return @call(.auto, Result.parse, .{ self, body } ++ args) catch return APIErrors.ParseFailed;
|
||||||
|
// return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
|
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
|
||||||
@ -508,80 +398,42 @@ pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
|
|||||||
self.token = new_token;
|
self.token = new_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getJsonString(object: json.ObjectMap, name: []const u8) ?[]const u8 {
|
pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId {
|
||||||
const value = object.get(name);
|
assert(code.len != 0);
|
||||||
if (value == null) {
|
|
||||||
return null;
|
for (0.., self.item_codes.items) |i, item_code| {
|
||||||
|
if (std.mem.eql(u8, code, item_code)) {
|
||||||
|
return @intCast(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.? != json.Value.string) {
|
const code_dupe = try self.allocator.dupe(u8, code);
|
||||||
return null;
|
errdefer self.allocator.free(code_dupe);
|
||||||
}
|
try self.item_codes.append(code_dupe);
|
||||||
|
|
||||||
return value.?.string;
|
return @intCast(self.item_codes.items.len - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dupeJsonString(allocator: Allocator, object: json.ObjectMap, name: []const u8) !?[]u8 {
|
pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
|
||||||
const str = getJsonString(object, name) orelse return null;
|
if (id >= self.item_codes.items.len) {
|
||||||
return try allocator.dupe(u8, str);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getJsonInteger(object: json.ObjectMap, name: []const u8) ?i64 {
|
|
||||||
const value = object.get(name);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.? != json.Value.integer) {
|
return self.item_codes.items[id];
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.?.integer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asJsonObject(value: json.Value) ?json.ObjectMap {
|
|
||||||
if (value != json.Value.object) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.object;
|
// ------------------------- Endpoints ------------------------
|
||||||
}
|
|
||||||
|
|
||||||
fn asJsonArray(value: json.Value) ?json.Array {
|
|
||||||
if (value != json.Value.array) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.array;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getJsonObject(object: json.ObjectMap, name: []const u8) ?json.ObjectMap {
|
|
||||||
const value = object.get(name);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return asJsonObject(value.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getJsonArray(object: json.ObjectMap, name: []const u8) ?json.Array {
|
|
||||||
const value = object.get(name);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return asJsonArray(value.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
|
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
|
||||||
return try self.fetchAndParseObject(ServerStatus, .GET, "/", null);
|
return try self.fetchAndParseObject(ServerStatus, .GET, "/", null, .{ self.allocator });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !Character {
|
pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !Character {
|
||||||
const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
|
const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
|
||||||
defer self.allocator.free(path);
|
defer self.allocator.free(path);
|
||||||
|
|
||||||
return try self.fetchAndParseObject(Character, .GET, path, null);
|
return try self.fetchAndParseObject(Character, .GET, path, null, .{ self.allocator });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
||||||
@ -598,7 +450,7 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
|||||||
return APIErrors.ParseFailed;
|
return APIErrors.ParseFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = asJsonArray(result.body.?) orelse return APIErrors.ParseFailed;
|
const body = json_utils.asArray(result.body.?) orelse return APIErrors.ParseFailed;
|
||||||
|
|
||||||
var characters = try std.ArrayList(Character).initCapacity(self.allocator, body.items.len);
|
var characters = try std.ArrayList(Character).initCapacity(self.allocator, body.items.len);
|
||||||
errdefer {
|
errdefer {
|
||||||
@ -609,13 +461,12 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (body.items) |character_json| {
|
for (body.items) |character_json| {
|
||||||
const character_obj = asJsonObject(character_json) orelse return APIErrors.ParseFailed;
|
const character_obj = json_utils.asObject(character_json) orelse return APIErrors.ParseFailed;
|
||||||
const char = Character.parse(self.allocator, character_obj) catch return APIErrors.ParseFailed;
|
const char = Character.parse(character_obj, self.allocator, self) catch return APIErrors.ParseFailed;
|
||||||
|
|
||||||
characters.appendAssumeCapacity(char);
|
characters.appendAssumeCapacity(char);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return CharacterList{
|
return CharacterList{
|
||||||
.allocator = self.allocator,
|
.allocator = self.allocator,
|
||||||
.items = characters.items
|
.items = characters.items
|
||||||
@ -626,7 +477,7 @@ pub fn actionFight(self: *ArtifactsAPI, name: []const u8) !FightResult {
|
|||||||
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
|
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name});
|
||||||
defer self.allocator.free(path);
|
defer self.allocator.free(path);
|
||||||
|
|
||||||
return try self.fetchAndParseObject(FightResult, .POST, path, null);
|
return try self.fetchAndParseObject(FightResult, .POST, path, null, .{ self.allocator });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult {
|
pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult {
|
||||||
@ -636,7 +487,7 @@ pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveRe
|
|||||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y});
|
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y});
|
||||||
defer self.allocator.free(payload);
|
defer self.allocator.free(payload);
|
||||||
|
|
||||||
return try self.fetchAndParseObject(MoveResult, .POST, path, payload);
|
return try self.fetchAndParseObject(MoveResult, .POST, path, payload, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u64) !GoldTransactionResult {
|
pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u64) !GoldTransactionResult {
|
||||||
@ -646,21 +497,15 @@ pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u6
|
|||||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
|
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
|
||||||
defer self.allocator.free(payload);
|
defer self.allocator.free(payload);
|
||||||
|
|
||||||
return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload);
|
return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn actionBankDeposit(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult {
|
pub fn actionBankDepositItem(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult {
|
||||||
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name});
|
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name});
|
||||||
defer self.allocator.free(path);
|
defer self.allocator.free(path);
|
||||||
|
|
||||||
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
|
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
|
||||||
defer self.allocator.free(payload);
|
defer self.allocator.free(payload);
|
||||||
|
|
||||||
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload);
|
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload, .{});
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *ArtifactsAPI) void {
|
|
||||||
self.client.deinit();
|
|
||||||
self.allocator.free(self.server);
|
|
||||||
if (self.token) |str| self.allocator.free(str);
|
|
||||||
}
|
}
|
||||||
|
259
src/character.zig
Normal file
259
src/character.zig
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("json_utils.zig");
|
||||||
|
const ArtifactsAPI = @import("artifacts.zig");
|
||||||
|
const parseDateTime = ArtifactsAPI.parseDateTime;
|
||||||
|
const ItemId = ArtifactsAPI.ItemId;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const Character = @This();
|
||||||
|
|
||||||
|
fn getItemId(api: *ArtifactsAPI, object: json.ObjectMap, name: []const u8) !?ItemId {
|
||||||
|
const code = try json_utils.getStringRequired(object, name);
|
||||||
|
if (code.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return try api.getItemId(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub const SkillStats = struct {
|
||||||
|
level: i64,
|
||||||
|
xp: i64,
|
||||||
|
max_xp: i64,
|
||||||
|
|
||||||
|
fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats {
|
||||||
|
return SkillStats{
|
||||||
|
.level = try json_utils.getIntegerRequired(object, level),
|
||||||
|
.xp = try json_utils.getIntegerRequired(object, xp),
|
||||||
|
.max_xp = try json_utils.getIntegerRequired(object, max_xp),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CombatStats = struct {
|
||||||
|
attack: i64,
|
||||||
|
damage: i64,
|
||||||
|
resistance: i64,
|
||||||
|
|
||||||
|
fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
|
||||||
|
return CombatStats{
|
||||||
|
.attack = try json_utils.getIntegerRequired(object, attack),
|
||||||
|
.damage = try json_utils.getIntegerRequired(object, damage),
|
||||||
|
.resistance = try json_utils.getIntegerRequired(object, resistance),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Equipment = struct {
|
||||||
|
pub const Consumable = struct {
|
||||||
|
id: ?ItemId,
|
||||||
|
quantity: i64,
|
||||||
|
|
||||||
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable {
|
||||||
|
return Consumable{
|
||||||
|
.id = try getItemId(api, obj, name),
|
||||||
|
.quantity = try json_utils.getIntegerRequired(obj, quantity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
weapon: ?ItemId,
|
||||||
|
shield: ?ItemId,
|
||||||
|
helmet: ?ItemId,
|
||||||
|
body_armor: ?ItemId,
|
||||||
|
leg_armor: ?ItemId,
|
||||||
|
boots: ?ItemId,
|
||||||
|
|
||||||
|
ring1: ?ItemId,
|
||||||
|
ring2: ?ItemId,
|
||||||
|
amulet: ?ItemId,
|
||||||
|
|
||||||
|
artifact1: ?ItemId,
|
||||||
|
artifact2: ?ItemId,
|
||||||
|
artifact3: ?ItemId,
|
||||||
|
|
||||||
|
consumable1: Consumable,
|
||||||
|
consumable2: Consumable,
|
||||||
|
|
||||||
|
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Equipment {
|
||||||
|
return Equipment{
|
||||||
|
.weapon = try getItemId(api, obj, "weapon_slot"),
|
||||||
|
.shield = try getItemId(api, obj, "shield_slot"),
|
||||||
|
.helmet = try getItemId(api, obj, "helmet_slot"),
|
||||||
|
.body_armor = try getItemId(api, obj, "body_armor_slot"),
|
||||||
|
.leg_armor = try getItemId(api, obj, "leg_armor_slot"),
|
||||||
|
.boots = try getItemId(api, obj, "boots_slot"),
|
||||||
|
.ring1 = try getItemId(api, obj, "ring1_slot"),
|
||||||
|
.ring2 = try getItemId(api, obj, "ring2_slot"),
|
||||||
|
.amulet = try getItemId(api, obj, "amulet_slot"),
|
||||||
|
.artifact1 = try getItemId(api, obj, "artifact1_slot"),
|
||||||
|
.artifact2 = try getItemId(api, obj, "artifact2_slot"),
|
||||||
|
.artifact3 = try getItemId(api, obj, "artifact3_slot"),
|
||||||
|
.consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"),
|
||||||
|
.consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Inventory = struct {
|
||||||
|
const slot_count = 20;
|
||||||
|
|
||||||
|
pub const Slot = struct {
|
||||||
|
id: ?ItemId,
|
||||||
|
quantity: i64,
|
||||||
|
|
||||||
|
fn parse(api: *ArtifactsAPI, slot_obj: json.ObjectMap) !Slot {
|
||||||
|
return Slot{
|
||||||
|
.id = try getItemId(api, slot_obj, "code"),
|
||||||
|
.quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
slots: [slot_count]Slot,
|
||||||
|
|
||||||
|
fn parse(api: *ArtifactsAPI, slots_array: json.Array) !Inventory {
|
||||||
|
assert(slots_array.items.len == Inventory.slot_count);
|
||||||
|
|
||||||
|
var inventory: Inventory = undefined;
|
||||||
|
|
||||||
|
for (0.., slots_array.items) |i, slot_value| {
|
||||||
|
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
|
||||||
|
inventory.slots[i] = try Slot.parse(api, slot_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findSlot(self: *Inventory, id: ItemId) ?*Slot {
|
||||||
|
for (&self.slots) |*slot| {
|
||||||
|
if (slot.id == id) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removeItem(self: *Inventory, id: ItemId, quantity: u64) void {
|
||||||
|
const slot = self.findSlot(id) orelse unreachable;
|
||||||
|
assert(slot.quantity >= quantity);
|
||||||
|
|
||||||
|
slot.quantity -= @intCast(quantity);
|
||||||
|
if (slot.quantity == 0) {
|
||||||
|
slot.id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void {
|
||||||
|
if (self.findSlot(id)) |slot| {
|
||||||
|
slot.quantity += @intCast(quantity);
|
||||||
|
} else {
|
||||||
|
var empty_slot: ?*Slot = null;
|
||||||
|
for (&self.slots) |*slot| {
|
||||||
|
if (slot.id == null) {
|
||||||
|
empty_slot = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(empty_slot != null);
|
||||||
|
empty_slot.?.id = id;
|
||||||
|
empty_slot.?.quantity = @intCast(quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
arena: *std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
name: []u8,
|
||||||
|
skin: []u8,
|
||||||
|
account: ?[]u8,
|
||||||
|
total_xp: i64,
|
||||||
|
gold: i64,
|
||||||
|
hp: i64,
|
||||||
|
haste: i64,
|
||||||
|
x: i64,
|
||||||
|
y: i64,
|
||||||
|
cooldown_expiration: f64,
|
||||||
|
|
||||||
|
combat: SkillStats,
|
||||||
|
mining: SkillStats,
|
||||||
|
woodcutting: SkillStats,
|
||||||
|
fishing: SkillStats,
|
||||||
|
weaponcrafting: SkillStats,
|
||||||
|
gearcrafting: SkillStats,
|
||||||
|
jewelrycrafting: SkillStats,
|
||||||
|
cooking: SkillStats,
|
||||||
|
|
||||||
|
water: CombatStats,
|
||||||
|
fire: CombatStats,
|
||||||
|
earth: CombatStats,
|
||||||
|
air: CombatStats,
|
||||||
|
|
||||||
|
equipment: Equipment,
|
||||||
|
|
||||||
|
inventory_max_items: i64,
|
||||||
|
inventory: Inventory,
|
||||||
|
|
||||||
|
pub fn parse(obj: json.ObjectMap, child_allocator: Allocator, api: *ArtifactsAPI) !Character {
|
||||||
|
var arena = try child_allocator.create(std.heap.ArenaAllocator);
|
||||||
|
errdefer child_allocator.destroy(arena);
|
||||||
|
|
||||||
|
arena.* = std.heap.ArenaAllocator.init(child_allocator);
|
||||||
|
errdefer arena.deinit();
|
||||||
|
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty;
|
||||||
|
const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty;
|
||||||
|
|
||||||
|
return Character{
|
||||||
|
.arena = arena,
|
||||||
|
.account = try json_utils.dupeString(allocator, obj, "account"),
|
||||||
|
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
|
||||||
|
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
|
||||||
|
|
||||||
|
.total_xp = try json_utils.getIntegerRequired(obj, "total_xp"),
|
||||||
|
.gold = try json_utils.getIntegerRequired(obj, "gold"),
|
||||||
|
.hp = try json_utils.getIntegerRequired(obj, "hp"),
|
||||||
|
.haste = try json_utils.getIntegerRequired(obj, "haste"),
|
||||||
|
.x = try json_utils.getIntegerRequired(obj, "x"),
|
||||||
|
.y = try json_utils.getIntegerRequired(obj, "y"),
|
||||||
|
.cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime,
|
||||||
|
|
||||||
|
.combat = try SkillStats.parse(obj, "level", "xp", "max_xp"),
|
||||||
|
.mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"),
|
||||||
|
.woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"),
|
||||||
|
.fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"),
|
||||||
|
.weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"),
|
||||||
|
.gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"),
|
||||||
|
.jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"),
|
||||||
|
.cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"),
|
||||||
|
|
||||||
|
.water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"),
|
||||||
|
.fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"),
|
||||||
|
.earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"),
|
||||||
|
.air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"),
|
||||||
|
|
||||||
|
.equipment = try Equipment.parse(api, obj),
|
||||||
|
|
||||||
|
.inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
|
||||||
|
.inventory = try Inventory.parse(api, inventory)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Character) void {
|
||||||
|
var child_allocator = self.arena.child_allocator;
|
||||||
|
self.arena.deinit();
|
||||||
|
child_allocator.destroy(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getItemCount(self: *const Character) u32 {
|
||||||
|
var count: u32 = 0;
|
||||||
|
for (self.inventory.slots) |slot| {
|
||||||
|
count += @intCast(slot.quantity);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
80
src/json_utils.zig
Normal file
80
src/json_utils.zig
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
pub fn getInteger(object: json.ObjectMap, name: []const u8) ?i64 {
|
||||||
|
const value = object.get(name);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.? != json.Value.integer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.?.integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIntegerRequired(object: json.ObjectMap, name: []const u8) !i64 {
|
||||||
|
return getInteger(object, name) orelse return error.MissingProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asObject(value: json.Value) ?json.ObjectMap {
|
||||||
|
if (value != json.Value.object) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asArray(value: json.Value) ?json.Array {
|
||||||
|
if (value != json.Value.array) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.array;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getObject(object: json.ObjectMap, name: []const u8) ?json.ObjectMap {
|
||||||
|
const value = object.get(name);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asObject(value.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getArray(object: json.ObjectMap, name: []const u8) ?json.Array {
|
||||||
|
const value = object.get(name);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asArray(value.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getString(object: json.ObjectMap, name: []const u8) ?[]const u8 {
|
||||||
|
const value = object.get(name);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.? != json.Value.string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.?.string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getStringRequired(object: json.ObjectMap, name: []const u8) ![]const u8 {
|
||||||
|
return getString(object, name) orelse return error.MissingProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dupeString(allocator: Allocator, object: json.ObjectMap, name: []const u8) !?[]u8 {
|
||||||
|
const str = getString(object, name) orelse return null;
|
||||||
|
return try allocator.dupe(u8, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dupeStringRequired(allocator: Allocator, object: json.ObjectMap, name: []const u8) ![]u8 {
|
||||||
|
return (try dupeString(allocator, object, name)) orelse return error.MissingProperty;
|
||||||
|
}
|
238
src/main.zig
238
src/main.zig
@ -1,42 +1,97 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const ArtifactsAPI = @import("artifacts.zig");
|
const ArtifactsAPI = @import("artifacts.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
fn waitForCooldown(remaining_seconds: i64) void {
|
const Position = struct {
|
||||||
if (remaining_seconds > 0) {
|
x: i64,
|
||||||
std.log.debug("Waiting for cooldown {}", .{remaining_seconds});
|
y: i64,
|
||||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(remaining_seconds)) + 1));
|
|
||||||
|
fn init(x: i64, y: i64) Position {
|
||||||
|
return Position{
|
||||||
|
.x = x,
|
||||||
|
.y = y
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eql(self: Position, other: Position) bool {
|
||||||
|
return self.x == other.x and self.y == other.y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const QueuedAction = union(enum) {
|
||||||
|
move: Position,
|
||||||
|
attack,
|
||||||
|
depositGold: u64,
|
||||||
|
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManagedCharacter = struct {
|
||||||
|
character: ArtifactsAPI.Character,
|
||||||
|
action_queue: std.ArrayList(QueuedAction),
|
||||||
|
cooldown_expires_at: f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn currentTime() f64 {
|
||||||
|
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
||||||
|
return timestamp / std.time.ms_per_s;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn depositIfNeeded(api: *ArtifactsAPI, char: *ArtifactsAPI.Character) !void {
|
const Manager = struct {
|
||||||
if (char.getItemCount() < char.inventory_max_items) return;
|
allocator: Allocator,
|
||||||
|
characters: std.ArrayList(ManagedCharacter),
|
||||||
|
api: *ArtifactsAPI,
|
||||||
|
|
||||||
{
|
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
|
||||||
const move_result = try api.actionMove(char.name, 4, 1);
|
return Manager{
|
||||||
defer move_result.deinit();
|
.allocator = allocator,
|
||||||
waitForCooldown(move_result.cooldown.remaining_seconds);
|
.api = api,
|
||||||
|
.characters = std.ArrayList(ManagedCharacter).init(allocator),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const deposit_gold = try api.actionBankDepositGold(char.name, @intCast(char.gold));
|
fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void {
|
||||||
defer deposit_gold.deinit();
|
try self.characters.append(ManagedCharacter{
|
||||||
waitForCooldown(deposit_gold.cooldown.remaining_seconds);
|
.character = character,
|
||||||
|
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
|
||||||
for (char.inventory.slots) |slot| {
|
.cooldown_expires_at = character.cooldown_expiration
|
||||||
if (slot.quantity == 0) continue;
|
});
|
||||||
|
|
||||||
const deposit_item = try api.actionBankDeposit(char.name, slot.code, @intCast(slot.quantity));
|
|
||||||
defer deposit_item.deinit();
|
|
||||||
waitForCooldown(deposit_item.cooldown.remaining_seconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
fn poll(self: *Manager) ?*ManagedCharacter {
|
||||||
const move_result = try api.actionMove(char.name, 0, 1);
|
if (self.characters.items.len == 0) return null;
|
||||||
defer move_result.deinit();
|
|
||||||
waitForCooldown(move_result.cooldown.remaining_seconds);
|
var earliest_expiration = self.characters.items[0].cooldown_expires_at;
|
||||||
|
|
||||||
|
var now = currentTime();
|
||||||
|
for (self.characters.items) |managed_character| {
|
||||||
|
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (earliest_expiration > now) {
|
||||||
|
const duration_s = earliest_expiration - now;
|
||||||
|
const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
|
||||||
|
std.log.debug("waiting for {d:.3}s", .{duration_s});
|
||||||
|
std.time.sleep(std.time.ns_per_ms * (duration_ms + 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
now = currentTime();
|
||||||
|
for (self.characters.items) |*managed_character| {
|
||||||
|
if (now >= managed_character.cooldown_expires_at) {
|
||||||
|
return managed_character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fn deinit(self: Manager) void {
|
||||||
|
for (self.characters.items) |managed_character| {
|
||||||
|
managed_character.action_queue.deinit();
|
||||||
|
}
|
||||||
|
self.characters.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@ -62,59 +117,110 @@ pub fn main() !void {
|
|||||||
return error.MissingToken;
|
return error.MissingToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manager = Manager.init(allocator, &api);
|
||||||
|
defer manager.deinit();
|
||||||
|
|
||||||
const characters = try api.listMyCharacters();
|
const characters = try api.listMyCharacters();
|
||||||
defer characters.deinit();
|
defer characters.deinit();
|
||||||
|
|
||||||
{
|
for (characters.items) |character| {
|
||||||
var longest_cooldown: i64 = 0;
|
try manager.addCharacter(character);
|
||||||
|
|
||||||
for (characters.items) |char| {
|
|
||||||
if (char.x == 0 and char.y == 1) continue;
|
|
||||||
|
|
||||||
const result = try api.actionMove(char.name, 0, 1);
|
|
||||||
defer result.deinit();
|
|
||||||
|
|
||||||
longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (longest_cooldown > 0) {
|
|
||||||
std.log.debug("Waiting for cooldown {}", .{longest_cooldown});
|
|
||||||
|
|
||||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std.log.info("Started main loop", .{});
|
const status = try api.getServerStatus();
|
||||||
while (true) {
|
defer status.deinit();
|
||||||
var results = std.ArrayList(ArtifactsAPI.FightResult).init(allocator);
|
|
||||||
defer {
|
|
||||||
for (results.items) |*result| {
|
|
||||||
result.deinit();
|
|
||||||
}
|
|
||||||
results.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (characters.items) |char| {
|
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
|
||||||
var result = try api.actionFight(char.name);
|
std.log.info("Characters online: {}", .{ status.characters_online });
|
||||||
errdefer result.deinit();
|
|
||||||
|
|
||||||
try results.append(result);
|
const chicken_pos = Position{ .x = 0, .y = 1 };
|
||||||
}
|
const bank_pos = Position{ .x = 4, .y = 1 };
|
||||||
|
|
||||||
{
|
std.log.info("Starting main loop", .{});
|
||||||
var longest_cooldown: i64 = 0;
|
while (manager.poll()) |char| {
|
||||||
for (results.items) |result| {
|
if (char.action_queue.items.len > 0) {
|
||||||
longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds);
|
const action = char.action_queue.items[0];
|
||||||
|
|
||||||
|
var cooldown: ArtifactsAPI.Cooldown = undefined;
|
||||||
|
switch (action) {
|
||||||
|
.attack => {
|
||||||
|
std.log.debug("{s} attacks", .{char.character.name});
|
||||||
|
var result = try api.actionFight(char.character.name);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
cooldown = result.cooldown;
|
||||||
|
char.character.gold += result.fight.gold;
|
||||||
|
for (result.fight.drops.slice()) |item| {
|
||||||
|
char.character.inventory.addItem(item.id, @intCast(item.quantity));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.move => |pos| {
|
||||||
|
std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y});
|
||||||
|
var result = try api.actionMove(char.character.name, pos.x, pos.y);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
cooldown = result.cooldown;
|
||||||
|
char.character.x = pos.x;
|
||||||
|
char.character.y = pos.y;
|
||||||
|
},
|
||||||
|
.depositGold => |quantity| {
|
||||||
|
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
|
||||||
|
var result = try api.actionBankDepositGold(char.character.name, quantity);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
cooldown = result.cooldown;
|
||||||
|
char.character.gold -= @intCast(quantity);
|
||||||
|
assert(char.character.gold >= 0);
|
||||||
|
},
|
||||||
|
.depositItem => |item| {
|
||||||
|
std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name});
|
||||||
|
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
|
||||||
|
var result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
cooldown = result.cooldown;
|
||||||
|
char.character.inventory.removeItem(item.id, item.quantity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (longest_cooldown > 0) {
|
char.cooldown_expires_at = cooldown.expiration;
|
||||||
std.log.debug("Waiting for cooldown {}", .{longest_cooldown});
|
|
||||||
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1));
|
_ = char.action_queue.orderedRemove(0);
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (results.items) |*result| {
|
var character_pos = Position.init(char.character.x, char.character.y);
|
||||||
try depositIfNeeded(&api, &result.character);
|
|
||||||
|
// Deposit items and gold to bank if full
|
||||||
|
if (char.character.getItemCount() == char.character.inventory_max_items) {
|
||||||
|
if (!character_pos.eql(bank_pos)) {
|
||||||
|
try char.action_queue.append(.{ .move = bank_pos });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char.character.inventory.slots) |slot| {
|
||||||
|
if (slot.quantity == 0) continue;
|
||||||
|
|
||||||
|
if (slot.id) |item_id| {
|
||||||
|
try char.action_queue.append(.{
|
||||||
|
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.character.gold > 0) {
|
||||||
|
try char.action_queue.append(.{ .depositGold = @intCast(char.character.gold) });
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go to chickens
|
||||||
|
if (!character_pos.eql(chicken_pos)) {
|
||||||
|
try char.action_queue.append(.{ .move = chicken_pos });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack chickens
|
||||||
|
try char.action_queue.append(.{ .attack = {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
src/timegm.c
Normal file
35
src/timegm.c
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#include "time.h"
|
||||||
|
|
||||||
|
// From: https://stackoverflow.com/a/58037981
|
||||||
|
|
||||||
|
// Algorithm: http://howardhinnant.github.io/date_algorithms.html
|
||||||
|
int days_from_epoch(int y, int m, int d)
|
||||||
|
{
|
||||||
|
y -= m <= 2;
|
||||||
|
int era = y / 400;
|
||||||
|
int yoe = y - era * 400; // [0, 399]
|
||||||
|
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
|
||||||
|
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
|
||||||
|
return era * 146097 + doe - 719468;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It does not modify broken-down time
|
||||||
|
time_t my_timegm(struct tm const* t)
|
||||||
|
{
|
||||||
|
int year = t->tm_year + 1900;
|
||||||
|
int month = t->tm_mon; // 0-11
|
||||||
|
if (month > 11)
|
||||||
|
{
|
||||||
|
year += month / 12;
|
||||||
|
month %= 12;
|
||||||
|
}
|
||||||
|
else if (month < 0)
|
||||||
|
{
|
||||||
|
int years_diff = (11 - month) / 12;
|
||||||
|
year -= years_diff;
|
||||||
|
month += 12 * years_diff;
|
||||||
|
}
|
||||||
|
int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday);
|
||||||
|
|
||||||
|
return 60 * (60 * (24L * days_since_epoch + t->tm_hour) + t->tm_min) + t->tm_sec;
|
||||||
|
}
|
3
src/timegm.h
Normal file
3
src/timegm.h
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#include "time.h"
|
||||||
|
|
||||||
|
time_t my_timegm(struct tm const* t);
|
Loading…
Reference in New Issue
Block a user