track state of inventory locally

This commit is contained in:
Rokas Puzonas 2024-08-07 00:53:59 +03:00
parent 72d70c5d43
commit 99aad39c57
7 changed files with 715 additions and 384 deletions

View File

@ -10,6 +10,9 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.addIncludePath(b.path("src"));
exe.addCSourceFile(.{ .file = b.path("src/timegm.c") });
b.installArtifact(exe);

View File

@ -3,31 +3,44 @@ const assert = std.debug.assert;
const json = std.json;
const Allocator = std.mem.Allocator;
const json_utils = @import("json_utils.zig");
pub const Character = @import("character.zig");
// Specification: https://api.artifactsmmo.com/docs
// TODO: Convert 'expiration' date time strings into date time objects
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 {
RequestFailed,
ParseFailed
};
const ServerStatus = struct {
// TODO: Parse the rest of the fields
allocator: Allocator,
status: []const u8,
version: []const u8,
characters_online: i64,
fn parse(allocator: Allocator, object: json.ObjectMap) !ServerStatus {
const status = getJsonString(object, "status") orelse return error.MissingStatus;
const version = getJsonString(object, "version") orelse return error.MissingVersion;
fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus {
_ = api;
return ServerStatus{
.allocator = allocator,
.status = try allocator.dupe(u8, status),
.version = try allocator.dupe(u8, version)
.characters_online = json_utils.getInteger(object, "characters_online") orelse return error.MissingProperty,
.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 const SkillStats = struct {
level: i64,
xp: i64,
max_xp: i64,
pub fn parseDateTime(datetime: []const u8) ?f64 {
const time_h = @cImport({
@cDefine("_XOPEN_SOURCE", "700");
@cInclude("stddef.h");
@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 {
return SkillStats{
.level = getJsonInteger(object, level) orelse return error.MissingProperty,
.xp = getJsonInteger(object, xp) orelse return error.MissingProperty,
.max_xp = getJsonInteger(object, max_xp) orelse return error.MissingProperty,
};
}
};
var buffer: [256]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
var allocator = fba.allocator();
pub const CombatStats = struct {
attack: i64,
damage: i64,
resistance: i64,
const datetime_z = allocator.dupeZ(u8, datetime) catch return null;
fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats {
return CombatStats{
.attack = getJsonInteger(object, attack) orelse return error.MissingProperty,
.damage = getJsonInteger(object, damage) orelse return error.MissingProperty,
.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)
};
var tm: time_h.tm = .{};
const s = time_h.strptime(datetime_z, "%FT%H:%M:%S.", &tm);
if (s == null) {
return null;
}
pub fn deinit(self: *Character) void {
var child_allocator = self.arena.child_allocator;
self.arena.deinit();
child_allocator.destroy(self.arena);
const s_len = std.mem.len(s);
if (s[s_len-1] != 'Z') {
return null;
}
pub fn getItemCount(self: *const Character) u32 {
var count: u32 = 0;
for (self.inventory.slots) |slot| {
count += @intCast(slot.quantity);
}
return count;
const milliseconds_str = s[0..(s_len-1)];
var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null);
while (milliseconds >= 1) {
milliseconds /= 10;
}
};
const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm));
return seconds + milliseconds;
}
pub const CharacterList = struct {
allocator: Allocator,
@ -305,44 +149,81 @@ pub const Cooldown = struct {
}
};
allocator: Allocator,
total_seconds: i64,
remaining_seconds: i64,
expiration: []u8,
expiration: f64,
reason: Reason,
fn parse(allocator: Allocator, obj: json.ObjectMap) !Cooldown {
const reason = getJsonString(obj, "reason") orelse return error.MissingProperty;
fn parse(obj: json.ObjectMap) !Cooldown {
const reason = try json_utils.getStringRequired(obj, "reason");
const expiration = try json_utils.getStringRequired(obj, "expiration");
return Cooldown{
.allocator = allocator,
.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,
.expiration = parseDateTime(expiration) orelse return error.InvalidDateTime,
.reason = Reason.parse(reason) orelse return error.UnknownReason
};
}
pub fn deinit(self: Cooldown) void {
self.allocator.free(self.expiration);
}
};
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,
character: Character,
fight: Details,
fn parse(allocator: Allocator, obj: json.ObjectMap) !FightResult {
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
const character = getJsonObject(obj, "character") orelse return error.MissingProperty;
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !FightResult {
const cooldown = json_utils.getObject(obj, "cooldown") 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{
.cooldown = try Cooldown.parse(allocator, cooldown),
.character = try Character.parse(allocator, character)
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(character, allocator, api),
.fight = try Details.parse(api, fight)
};
}
pub fn deinit(self: *FightResult) void {
self.cooldown.deinit();
self.character.deinit();
}
};
@ -350,48 +231,52 @@ pub const FightResult = struct {
pub const MoveResult = struct {
cooldown: Cooldown,
fn parse(allocator: Allocator, obj: json.ObjectMap) !MoveResult {
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
return MoveResult{
.cooldown = try Cooldown.parse(allocator, cooldown)
.cooldown = try Cooldown.parse(cooldown)
};
}
pub fn deinit(self: MoveResult) void {
self.cooldown.deinit();
_ = self;
}
};
pub const GoldTransactionResult = struct {
cooldown: Cooldown,
fn parse(allocator: Allocator, obj: json.ObjectMap) !GoldTransactionResult {
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
return GoldTransactionResult{
.cooldown = try Cooldown.parse(allocator, cooldown)
.cooldown = try Cooldown.parse(cooldown)
};
}
pub fn deinit(self: GoldTransactionResult) void {
self.cooldown.deinit();
_ = self;
}
};
pub const ItemTransactionResult = struct {
cooldown: Cooldown,
fn parse(allocator: Allocator, obj: json.ObjectMap) !ItemTransactionResult {
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult {
_ = api;
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
return ItemTransactionResult{
.cooldown = try Cooldown.parse(allocator, cooldown)
.cooldown = try Cooldown.parse(cooldown)
};
}
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 {
const server = try allocator.dupe(u8, "https://api.artifactsmmo.com");
const server_uri = std.Uri.parse(server) catch unreachable;
return ArtifactsAPI{
.allocator = allocator,
.item_codes = std.ArrayList([]u8).init(allocator),
.client = .{ .allocator = allocator },
.server = server,
.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 {
var uri = self.server_uri;
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);
defer result.deinit();
@ -483,8 +372,9 @@ fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Metho
return APIErrors.ParseFailed;
}
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed;
const body = json_utils.asObject(result.body.?) orelse 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 {
@ -508,80 +398,42 @@ pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
self.token = new_token;
}
fn getJsonString(object: json.ObjectMap, name: []const u8) ?[]const u8 {
const value = object.get(name);
if (value == null) {
return null;
pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId {
assert(code.len != 0);
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) {
return null;
}
const code_dupe = try self.allocator.dupe(u8, code);
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 {
const str = getJsonString(object, name) orelse return null;
return try allocator.dupe(u8, str);
}
fn getJsonInteger(object: json.ObjectMap, name: []const u8) ?i64 {
const value = object.get(name);
if (value == null) {
pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 {
if (id >= self.item_codes.items.len) {
return null;
}
if (value.? != json.Value.integer) {
return null;
}
return value.?.integer;
return self.item_codes.items[id];
}
fn asJsonObject(value: json.Value) ?json.ObjectMap {
if (value != json.Value.object) {
return null;
}
return value.object;
}
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.?);
}
// ------------------------- Endpoints ------------------------
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 {
const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name});
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 {
@ -598,7 +450,7 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
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);
errdefer {
@ -609,13 +461,12 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
}
for (body.items) |character_json| {
const character_obj = asJsonObject(character_json) orelse return APIErrors.ParseFailed;
const char = Character.parse(self.allocator, character_obj) catch return APIErrors.ParseFailed;
const character_obj = json_utils.asObject(character_json) orelse return APIErrors.ParseFailed;
const char = Character.parse(character_obj, self.allocator, self) catch return APIErrors.ParseFailed;
characters.appendAssumeCapacity(char);
}
return CharacterList{
.allocator = self.allocator,
.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});
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 {
@ -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});
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 {
@ -646,21 +497,15 @@ pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u6
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity});
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});
defer self.allocator.free(path);
const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity});
defer self.allocator.free(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);
return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload, .{});
}

259
src/character.zig Normal file
View 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
View 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;
}

View File

@ -1,42 +1,97 @@
const std = @import("std");
const assert = std.debug.assert;
const ArtifactsAPI = @import("artifacts.zig");
const Allocator = std.mem.Allocator;
fn waitForCooldown(remaining_seconds: i64) void {
if (remaining_seconds > 0) {
std.log.debug("Waiting for cooldown {}", .{remaining_seconds});
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(remaining_seconds)) + 1));
const Position = struct {
x: i64,
y: i64,
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 {
if (char.getItemCount() < char.inventory_max_items) return;
const Manager = struct {
allocator: Allocator,
characters: std.ArrayList(ManagedCharacter),
api: *ArtifactsAPI,
{
const move_result = try api.actionMove(char.name, 4, 1);
defer move_result.deinit();
waitForCooldown(move_result.cooldown.remaining_seconds);
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
.api = api,
.characters = std.ArrayList(ManagedCharacter).init(allocator),
};
}
const deposit_gold = try api.actionBankDepositGold(char.name, @intCast(char.gold));
defer deposit_gold.deinit();
waitForCooldown(deposit_gold.cooldown.remaining_seconds);
for (char.inventory.slots) |slot| {
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 addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void {
try self.characters.append(ManagedCharacter{
.character = character,
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
.cooldown_expires_at = character.cooldown_expiration
});
}
{
const move_result = try api.actionMove(char.name, 0, 1);
defer move_result.deinit();
waitForCooldown(move_result.cooldown.remaining_seconds);
fn poll(self: *Manager) ?*ManagedCharacter {
if (self.characters.items.len == 0) return null;
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 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -62,59 +117,110 @@ pub fn main() !void {
return error.MissingToken;
}
var manager = Manager.init(allocator, &api);
defer manager.deinit();
const characters = try api.listMyCharacters();
defer characters.deinit();
{
var longest_cooldown: i64 = 0;
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));
}
for (characters.items) |character| {
try manager.addCharacter(character);
}
std.log.info("Started main loop", .{});
while (true) {
var results = std.ArrayList(ArtifactsAPI.FightResult).init(allocator);
defer {
for (results.items) |*result| {
result.deinit();
}
results.deinit();
}
const status = try api.getServerStatus();
defer status.deinit();
for (characters.items) |char| {
var result = try api.actionFight(char.name);
errdefer result.deinit();
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
std.log.info("Characters online: {}", .{ status.characters_online });
try results.append(result);
}
const chicken_pos = Position{ .x = 0, .y = 1 };
const bank_pos = Position{ .x = 4, .y = 1 };
{
var longest_cooldown: i64 = 0;
for (results.items) |result| {
longest_cooldown = @max(longest_cooldown, result.cooldown.remaining_seconds);
std.log.info("Starting main loop", .{});
while (manager.poll()) |char| {
if (char.action_queue.items.len > 0) {
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) {
std.log.debug("Waiting for cooldown {}", .{longest_cooldown});
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(longest_cooldown)) + 1));
}
char.cooldown_expires_at = cooldown.expiration;
_ = char.action_queue.orderedRemove(0);
continue;
}
for (results.items) |*result| {
try depositIfNeeded(&api, &result.character);
var character_pos = Position.init(char.character.x, char.character.y);
// 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
View 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
View File

@ -0,0 +1,3 @@
#include "time.h"
time_t my_timegm(struct tm const* t);