initial commit

This commit is contained in:
Rokas Puzonas 2024-07-31 01:12:40 +03:00
commit 56de31d5c5
5 changed files with 614 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
zig-cache
zig-out

35
build.zig Normal file
View File

@ -0,0 +1,35 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "artificer",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
}

7
build.zig.zon Normal file
View File

@ -0,0 +1,7 @@
.{
.name = "artificer",
.version = "0.1.0",
.minimum_zig_version = "0.12.0",
.dependencies = .{ },
.paths = .{ "" },
}

544
src/artifacts.zig Normal file
View File

@ -0,0 +1,544 @@
const std = @import("std");
const json = std.json;
const Allocator = std.mem.Allocator;
// Specification: https://api.artifactsmmo.com/docs
// TODO: Convert 'expiration' date time strings into date time objects
const ArtifactsAPI = @This();
pub const APIErrors = error {
RequestFailed,
ParseFailed
};
const ServerStatus = struct {
// TODO: Parse the rest of the fields
allocator: Allocator,
status: []const u8,
version: []const u8,
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;
return ServerStatus{
.allocator = allocator,
.status = try allocator.dupe(u8, status),
.version = try allocator.dupe(u8, version)
};
}
pub fn deinit(self: ServerStatus) void {
self.allocator.free(self.status);
self.allocator.free(self.version);
}
};
pub const Character = struct {
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 = 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,
};
}
};
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 = 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"),
};
}
};
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,
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();
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)
};
}
pub fn deinit(self: *Character) void {
var child_allocator = self.arena.child_allocator;
self.arena.deinit();
child_allocator.destroy(self.arena);
}
};
pub const CharacterList = struct {
allocator: Allocator,
items: []Character,
pub fn deinit(self: CharacterList) void {
for (self.items) |*char| {
char.deinit();
}
self.allocator.free(self.items);
}
};
pub const Cooldown = struct {
pub const Reason = enum {
movement,
fight,
crafting,
gathering,
buy_ge,
sell_ge,
delete_item,
deposit_bank,
withdraw_bank,
equip,
unequip,
task,
recycling,
fn parse(str: []const u8) ?Reason {
const eql = std.mem.eql;
if (eql(u8, str, "movement")) {
return .movement;
} else if (eql(u8, str, "fight")) {
return .fight;
} else if (eql(u8, str, "crafting")) {
return .crafting;
} else if (eql(u8, str, "gathering")) {
return .gathering;
} else if (eql(u8, str, "buy_ge")) {
return .buy_ge;
} else if (eql(u8, str, "sell_ge")) {
return .sell_ge;
} else if (eql(u8, str, "delete_item")) {
return .delete_item;
} else if (eql(u8, str, "deposit_bank")) {
return .deposit_bank;
} else if (eql(u8, str, "withdraw_bank")) {
return .withdraw_bank;
} else if (eql(u8, str, "equip")) {
return .equip;
} else if (eql(u8, str, "unequip")) {
return .unequip;
} else if (eql(u8, str, "task")) {
return .task;
} else if (eql(u8, str, "recycling")) {
return .recycling;
} else {
return null;
}
}
};
allocator: Allocator,
total_seconds: i64,
remaining_seconds: i64,
expiration: []u8,
reason: Reason,
fn parse(allocator: Allocator, obj: json.ObjectMap) !Cooldown {
const reason = getJsonString(obj, "reason") orelse return error.MissingProperty;
return Cooldown{
.allocator = allocator,
.total_seconds = getJsonInteger(obj, "totalSeconds") orelse return error.MissingProperty,
.remaining_seconds = getJsonInteger(obj, "remainingSeconds") orelse return error.MissingProperty,
.expiration = (try dupeJsonString(allocator, obj, "expiration")) orelse return error.MissingProperty,
.reason = Reason.parse(reason) orelse return error.UnknownReason
};
}
pub fn deinit(self: Cooldown) void {
self.allocator.free(self.expiration);
}
};
pub const FightResult = struct {
cooldown: Cooldown,
fn parse(allocator: Allocator, obj: json.ObjectMap) !FightResult {
const cooldown = getJsonObject(obj, "cooldown") orelse return error.MissingProperty;
return FightResult{
.cooldown = try Cooldown.parse(allocator, cooldown)
};
}
pub fn deinit(self: FightResult) void {
self.cooldown.deinit();
}
};
const ArtifactsFetchResult = struct {
arena: std.heap.ArenaAllocator,
status: std.http.Status,
body: ?json.Value = null,
fn deinit(self: ArtifactsFetchResult) void {
self.arena.deinit();
}
};
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,
.client = .{ .allocator = allocator },
.server = server,
.server_uri = server_uri
};
}
fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8) !ArtifactsFetchResult {
var uri = self.server_uri;
uri.path = .{ .raw = path };
var arena = std.heap.ArenaAllocator.init(self.allocator);
errdefer arena.deinit();
var response_storage = std.ArrayList(u8).init(arena.allocator());
var opts = std.http.Client.FetchOptions{
.method = method,
.location = .{ .uri = uri },
.response_storage = .{ .dynamic = &response_storage }
};
var authorization_header: ?[]u8 = null;
defer if (authorization_header) |str| self.allocator.free(str);
if (self.token) |token| {
authorization_header = try std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token});
opts.headers.authorization = .{ .override = authorization_header.? };
}
const result = try self.client.fetch(opts);
if (result.status != .ok) {
return ArtifactsFetchResult{
.arena = arena,
.status = result.status
};
}
const response_body = response_storage.items;
const parsed = try json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed });
if (parsed != json.Value.object) {
return APIErrors.ParseFailed;
}
return ArtifactsFetchResult{
.status = result.status,
.arena = arena,
.body = parsed.object.get("data")
};
}
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
const url_dupe = self.allocator.dupe(u8, url);
errdefer self.allocator.free(url_dupe);
const uri = try std.Uri.parse(url_dupe);
self.allocator.free(self.server);
self.server = url_dupe;
self.server_uri = uri;
}
pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void {
var new_token: ?[]u8 = null;
if (token != null) {
new_token = try self.allocator.dupe(u8, token.?);
}
if (self.token) |str| self.allocator.free(str);
self.token = new_token;
}
fn getJsonString(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;
}
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) {
return null;
}
if (value.? != json.Value.integer) {
return null;
}
return value.?.integer;
}
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.?);
}
pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus {
const result = try self.fetch(.GET, "/");
defer result.deinit();
if (result.status != .ok) {
return APIErrors.RequestFailed;
}
if (result.body == null) {
return APIErrors.ParseFailed;
}
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
return ServerStatus.parse(self.allocator, body) catch return APIErrors.ParseFailed;
}
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);
const result = try self.fetch(.GET, path);
defer result.deinit();
if (result.status != .ok) {
return APIErrors.RequestFailed;
}
if (result.body == null) {
return APIErrors.ParseFailed;
}
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
return Character.parse(self.allocator, body) catch return APIErrors.ParseFailed;
}
pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList {
const path = try std.fmt.allocPrint(self.allocator, "/my/characters", .{});
defer self.allocator.free(path);
const result = try self.fetch(.GET, path);
defer result.deinit();
if (result.status != .ok) {
return APIErrors.RequestFailed;
}
if (result.body == null) {
return APIErrors.ParseFailed;
}
const body = asJsonArray(result.body.?) orelse return APIErrors.ParseFailed;
var characters = try std.ArrayList(Character).initCapacity(self.allocator, body.items.len);
errdefer {
for (characters.items) |*char| {
char.deinit();
}
characters.deinit();
}
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;
characters.appendAssumeCapacity(char);
}
return CharacterList{
.allocator = self.allocator,
.items = characters.items
};
}
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);
const result = try self.fetch(.POST, path);
defer result.deinit();
if (result.status != .ok) {
return APIErrors.RequestFailed;
}
if (result.body == null) {
return APIErrors.ParseFailed;
}
const body = asJsonObject(result.body.?) orelse return APIErrors.ParseFailed;
return FightResult.parse(self.allocator, body) catch return APIErrors.ParseFailed;
}
pub fn deinit(self: *ArtifactsAPI) void {
self.client.deinit();
self.allocator.free(self.server);
if (self.token) |str| self.allocator.free(str);
}

26
src/main.zig Normal file
View File

@ -0,0 +1,26 @@
const std = @import("std");
const ArtifactsAPI = @import("artifacts.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var api = try ArtifactsAPI.init(allocator);
defer api.deinit();
{ // Set auth token from environment variable
var env = try std.process.getEnvMap(allocator);
defer env.deinit();
const token = env.get("ARTIFACTS_TOKEN");
try api.setToken(token);
}
while (true) {
const result = try api.actionFight("Daisy");
defer result.deinit();
std.time.sleep(std.time.ns_per_s * (@as(u64, @intCast(result.cooldown.remaining_seconds)) + 1));
}
}